diff --git a/ajax.php b/ajax.php
index a66a04f..2720fd0 100755
--- a/ajax.php
+++ b/ajax.php
@@ -50,10 +50,7 @@ function show_all_improvise_questions(jazzquiz $jazzquiz): array {
$improviser = new improviser($jazzquiz);
$questionrecords = $improviser->get_all_improvised_question_definitions();
if (!$questionrecords) {
- return [
- 'status' => 'error',
- 'message' => 'No improvisation questions',
- ];
+ return ['status' => 'error', 'message' => 'No improvisation questions'];
}
$questions = [];
foreach ($questionrecords as $question) {
@@ -64,10 +61,7 @@ function show_all_improvise_questions(jazzquiz $jazzquiz): array {
'time' => $jazzquiz->data->defaultquestiontime,
];
}
- return [
- 'status' => 'success',
- 'questions' => $questions,
- ];
+ return ['status' => 'success', 'questions' => $questions];
}
/**
@@ -93,10 +87,7 @@ function show_all_jump_questions(jazzquiz $jazzquiz): array {
'time' => $question->time,
];
}
- return [
- 'status' => 'success',
- 'questions' => $questions,
- ];
+ return ['status' => 'success', 'questions' => $questions];
}
/**
@@ -105,7 +96,7 @@ function show_all_jump_questions(jazzquiz $jazzquiz): array {
* @param jazzquiz_session $session
* @return array
*/
-function get_question_form(jazzquiz_session $session): array {
+function get_question_form(jazzquiz_session $session, jazzquiz $jazzquiz): array {
$session->load_session_questions();
$slot = optional_param('slot', 0, PARAM_INT);
if ($slot === 0) {
@@ -116,7 +107,6 @@ function get_question_form(jazzquiz_session $session): array {
$css = [];
$isalreadysubmitted = true;
if (!$session->myattempt->has_responded($slot)) {
- $jazzquiz = $session->jazzquiz;
/** @var output\renderer $renderer */
$renderer = $jazzquiz->renderer;
$isinstructor = $jazzquiz->is_instructor();
@@ -141,7 +131,7 @@ function get_question_form(jazzquiz_session $session): array {
* @param jazzquiz_session $session
* @return array
*/
-function start_question(jazzquiz_session $session): array {
+function start_question(jazzquiz_session $session, jazzquiz $jazzquiz): array {
$session->load_session_questions();
$session->load_attempts();
$method = required_param('method', PARAM_ALPHA);
@@ -152,70 +142,53 @@ function start_question(jazzquiz_session $session): array {
$questiontime = optional_param('questiontime', 0, PARAM_INT);
$jazzquizquestionid = optional_param('jazzquizquestionid', 0, PARAM_INT);
if ($jazzquizquestionid !== 0) {
- $jazzquizquestion = $session->jazzquiz->get_question_by_id($jazzquizquestionid);
+ $jazzquizquestion = $jazzquiz->get_question_by_id($jazzquizquestionid);
if ($jazzquizquestion) {
- $session->data->slot = $jazzquizquestion->data->slot;
+ $session->slot = $jazzquizquestion->data->slot;
}
}
break;
case 'repoll':
$lastslot = count($session->questions);
if ($lastslot === 0) {
- return [
- 'status' => 'error',
- 'message' => 'Nothing to repoll.',
- ];
+ return ['status' => 'error', 'message' => 'Nothing to repoll.'];
}
$questionid = $session->questions[$lastslot]->questionid;
- $questiontime = $session->data->currentquestiontime;
+ $questiontime = $session->currentquestiontime;
break;
case 'next':
- $lastslot = count($session->jazzquiz->questions);
- if ($session->data->slot >= $lastslot) {
- return [
- 'status' => 'error',
- 'message' => 'No next question.',
- ];
+ $lastslot = count($jazzquiz->questions);
+ if ($session->slot >= $lastslot) {
+ return ['status' => 'error', 'message' => 'No next question.'];
}
- $session->data->slot++;
- $jazzquizquestion = $session->jazzquiz->questions[$session->data->slot];
+ $session->slot++;
+ $jazzquizquestion = $jazzquiz->questions[$session->slot];
$questionid = $jazzquizquestion->question->id;
$questiontime = $jazzquizquestion->data->questiontime;
break;
case 'random':
- $slots = $session->get_unasked_slots();
+ $slots = $session->get_unasked_slots($jazzquiz->questions);
if (count($slots) > 0) {
- $session->data->slot = $slots[array_rand($slots)];
+ $session->slot = $slots[array_rand($slots)];
} else {
- $lastslot = count($session->jazzquiz->questions);
- $session->data->slot = random_int(1, $lastslot);
+ $lastslot = count($jazzquiz->questions);
+ $session->slot = random_int(1, $lastslot);
}
- $jazzquizquestion = $session->jazzquiz->questions[$session->data->slot];
+ $jazzquizquestion = $jazzquiz->questions[$session->slot];
$questionid = $jazzquizquestion->question->id;
$questiontime = $jazzquizquestion->data->questiontime;
break;
default:
- return [
- 'status' => 'error',
- 'message' => "Invalid method $method",
- ];
+ return ['status' => 'error', 'message' => "Invalid method $method"];
}
- list($success, $questiontime) = $session->start_question($questionid, $questiontime);
+ [$success, $questiontime] = $session->start_question($questionid, $questiontime, $jazzquiz->data->waitforquestiontime);
if (!$success) {
- return [
- 'status' => 'error',
- 'message' => "Failed to start question $questionid for session",
- ];
+ return ['status' => 'error', 'message' => "Failed to start question $questionid for session"];
}
- $session->data->status = 'running';
+ $session->status = 'running';
$session->save();
-
- return [
- 'status' => 'success',
- 'questiontime' => $questiontime,
- 'delay' => $session->data->nextstarttime - time(),
- ];
+ return ['status' => 'success', 'questiontime' => $questiontime, 'delay' => $session->nextstarttime - time()];
}
/**
@@ -225,13 +198,10 @@ function start_question(jazzquiz_session $session): array {
* @return array
*/
function start_quiz(jazzquiz_session $session): array {
- if ($session->data->status !== 'notrunning') {
- return [
- 'status' => 'error',
- 'message' => 'Quiz is already running',
- ];
+ if ($session->status !== 'notrunning') {
+ return ['status' => 'error', 'message' => 'Quiz is already running'];
}
- $session->data->status = 'preparing';
+ $session->status = 'preparing';
$session->save();
return ['status' => 'success'];
}
@@ -242,26 +212,20 @@ function start_quiz(jazzquiz_session $session): array {
* @param jazzquiz_session $session
* @return array
*/
-function save_question(jazzquiz_session $session): array {
+function save_question(jazzquiz_session $session, jazzquiz $jazzquiz): array {
$attempt = $session->myattempt;
if (!$attempt->belongs_to_current_user()) {
- return [
- 'status' => 'error',
- 'message' => 'Invalid user',
- ];
+ return ['status' => 'error', 'message' => 'Invalid user'];
}
$session->load_session_questions();
$attempt->save_question(count($session->questions));
$session->update_attendance_for_current_user();
// Only give feedback if specified in session.
$feedback = '';
- if ($session->data->showfeedback) {
- $feedback = $attempt->get_question_feedback($session->jazzquiz);
+ if ($session->showfeedback) {
+ $feedback = $attempt->get_question_feedback($jazzquiz);
}
- return [
- 'status' => 'success',
- 'feedback' => $feedback,
- ];
+ return ['status' => 'success', 'feedback' => $feedback];
}
/**
@@ -274,19 +238,16 @@ function run_voting(jazzquiz_session $session): array {
$questions = required_param('questions', PARAM_RAW);
$questions = json_decode(urldecode($questions), true);
if (!$questions) {
- return [
- 'status' => 'error',
- 'message' => 'Failed to decode questions',
- ];
+ return ['status' => 'error', 'message' => 'Failed to decode questions'];
}
$qtype = optional_param('question_type', '', PARAM_ALPHANUM);
$session->load_session_questions();
- $vote = new jazzquiz_vote($session->data->id);
+ $vote = new jazzquiz_vote($session->id);
$slot = count($session->questions);
- $vote->prepare_options($session->jazzquiz->data->id, $qtype, $questions, $slot);
+ $vote->prepare_options($session->jazzquizid, $qtype, $questions, $slot);
- $session->data->status = 'voting';
+ $session->status = jazzquiz_session::STATUS_VOTING;
$session->save();
return ['status' => 'success'];
}
@@ -299,7 +260,7 @@ function run_voting(jazzquiz_session $session): array {
*/
function save_vote(jazzquiz_session $session): array {
$voteid = required_param('vote', PARAM_INT);
- $vote = new jazzquiz_vote($session->data->id);
+ $vote = new jazzquiz_vote($session->id);
$status = $vote->save_vote($voteid);
return ['status' => ($status ? 'success' : 'error')];
}
@@ -313,7 +274,7 @@ function get_vote_results(jazzquiz_session $session): array {
$session->load_session_questions();
$session->load_attempts();
$slot = count($session->questions);
- $vote = new jazzquiz_vote($session->data->id, $slot);
+ $vote = new jazzquiz_vote($session->id, $slot);
$votes = $vote->get_results();
return [
'answers' => $votes,
@@ -328,7 +289,7 @@ function get_vote_results(jazzquiz_session $session): array {
* @return array
*/
function end_question(jazzquiz_session $session): array {
- $session->data->status = 'reviewing';
+ $session->status = jazzquiz_session::STATUS_REVIEWING;
$session->save();
return ['status' => 'success'];
}
@@ -339,9 +300,9 @@ function end_question(jazzquiz_session $session): array {
* @param jazzquiz_session $session
* @return array
*/
-function get_right_response(jazzquiz_session $session): array {
+function get_right_response(jazzquiz_session $session, jazzquiz $jazzquiz): array {
$session->load_session_questions();
- return ['right_answer' => $session->get_question_right_response()];
+ return ['right_answer' => $session->get_question_right_response($jazzquiz)];
}
/**
@@ -368,10 +329,10 @@ function get_results(jazzquiz_session $session): array {
$slot = count($session->questions);
$qtype = $session->get_question_type_by_slot($slot);
$results = $session->get_question_results_list($slot);
- list($results['responses'], $mergecount) = $session->get_merged_responses($slot, $results['responses']);
+ [$results['responses'], $mergecount] = $session->get_merged_responses($slot, $results['responses']);
// Check if this has been voted on before.
- $vote = new jazzquiz_vote($session->data->id, $slot);
+ $vote = new jazzquiz_vote($session->id, $slot);
$hasvotes = count($vote->get_results()) > 0;
return [
@@ -429,93 +390,14 @@ function stack_to_latex(): array {
$input = urldecode($input);
$question = $DB->get_record_sql('SELECT id FROM {question} WHERE qtype = ? AND name LIKE ?', ['stack', '{IMPROV}%']);
if (!$question) {
- return [
- 'message' => 'STACK question not found.',
- 'latex' => $input,
- 'original' => $input,
- ];
+ return ['message' => 'STACK question not found.', 'latex' => $input, 'original' => $input];
}
-
/** @var \qtype_stack_question $question */
$question = question_bank::load_question($question->id);
$question->initialise_question_from_seed();
$state = $question->get_input_state('ans1', ['ans1' => $input]);
$latex = $state->contentsdisplayed;
-
- return [
- 'latex' => $latex,
- 'original' => $input,
- ];
-}
-
-/**
- * Retrieve the current state of the session.
- *
- * @param jazzquiz_session $session
- * @return array
- */
-function session_info(jazzquiz_session $session): array {
- global $DB;
- switch ($session->data->status) {
- // Just a generic response with the state.
- case 'notrunning':
- if ($session->jazzquiz->is_instructor()) {
- $session = new jazzquiz_session($session->jazzquiz, $session->data->id);
- $session->load_attempts();
- return [
- 'status' => $session->data->status,
- 'student_count' => $session->get_student_count(),
- ];
- }
- // Fall-through.
- case 'preparing':
- case 'reviewing':
- return [
- 'status' => $session->data->status,
- 'slot' => $session->data->slot, // For the preplanned questions.
- ];
-
- case 'voting':
- $voteoptions = $DB->get_records('jazzquiz_votes', ['sessionid' => $session->data->id]);
- $options = [];
- $html = '
';
- $i = 0;
- foreach ($voteoptions as $voteoption) {
- $options[] = [
- 'text' => $voteoption->attempt,
- 'id' => $voteoption->id,
- 'question_type' => $voteoption->qtype,
- 'content_id' => "vote_answer_label_$i",
- ];
- $html .= '';
- $html .= ' ';
- $html .= '' . $voteoption->attempt . ' ';
- $html .= ' ';
- $i++;
- }
- $html .= '
';
- $html .= 'Save ';
- return [
- 'status' => 'voting',
- 'html' => $html,
- 'options' => $options,
- ];
-
- // Send the currently active question.
- case 'running':
- return [
- 'status' => 'running',
- 'questiontime' => $session->data->currentquestiontime,
- 'delay' => $session->data->nextstarttime - time(),
- ];
-
- // This should not be reached, but if it ever is, let's just assume the quiz is not running.
- default:
- return [
- 'status' => 'notrunning',
- 'message' => 'Unknown error. State: ' . $session->data->status,
- ];
- }
+ return ['latex' => $latex, 'original' => $input];
}
/**
@@ -525,23 +407,22 @@ function session_info(jazzquiz_session $session): array {
* @param jazzquiz_session $session
* @return array
*/
-function handle_instructor_request(string $action, jazzquiz_session $session): array {
+function handle_instructor_request(string $action, jazzquiz_session $session, jazzquiz $jazzquiz): array {
return match ($action) {
'start_quiz' => start_quiz($session),
- 'get_question_form' => get_question_form($session),
- 'save_question' => save_question($session),
- 'list_improvise_questions' => show_all_improvise_questions($session->jazzquiz),
- 'list_jump_questions' => show_all_jump_questions($session->jazzquiz),
+ 'get_question_form' => get_question_form($session, $jazzquiz),
+ 'save_question' => save_question($session, $jazzquiz),
+ 'list_improvise_questions' => show_all_improvise_questions($jazzquiz),
+ 'list_jump_questions' => show_all_jump_questions($jazzquiz),
'run_voting' => run_voting($session),
'get_vote_results' => get_vote_results($session),
'get_results' => get_results($session),
- 'start_question' => start_question($session),
+ 'start_question' => start_question($session, $jazzquiz),
'end_question' => end_question($session),
- 'get_right_response' => get_right_response($session),
+ 'get_right_response' => get_right_response($session, $jazzquiz),
'merge_responses' => merge_responses($session),
'undo_merge' => undo_merge($session),
'close_session' => close_session($session),
- 'info' => session_info($session),
default => ['status' => 'error', 'message' => 'Invalid action'],
};
}
@@ -553,12 +434,11 @@ function handle_instructor_request(string $action, jazzquiz_session $session): a
* @param jazzquiz_session $session
* @return array
*/
-function handle_student_request(string $action, jazzquiz_session $session): array {
+function handle_student_request(string $action, jazzquiz_session $session, jazzquiz $jazzquiz): array {
return match ($action) {
- 'save_question' => save_question($session),
+ 'save_question' => save_question($session, $jazzquiz),
'save_vote' => save_vote($session),
- 'get_question_form' => get_question_form($session),
- 'info' => session_info($session),
+ 'get_question_form' => get_question_form($session, $jazzquiz),
default => ['status' => 'error', 'message' => 'Invalid action'],
};
}
@@ -580,37 +460,25 @@ function jazzquiz_ajax(): array {
$sessionid = required_param('sessionid', PARAM_INT);
$jazzquiz = new jazzquiz($cmid);
- $session = new jazzquiz_session($jazzquiz, $sessionid);
- if (!$session->data->sessionopen) {
- return [
- 'status' => 'sessionclosed',
- 'message' => 'Session is closed',
- ];
+ $session = new jazzquiz_session(null, $sessionid);
+ if (!$session->sessionopen) {
+ return ['status' => 'sessionclosed', 'message' => 'Session is closed'];
}
$session->load_my_attempt();
if (!$session->myattempt) {
- return [
- 'status' => 'error',
- 'message' => 'No attempt found',
- ];
+ return ['status' => 'error', 'message' => 'No attempt found'];
}
if (!$session->myattempt->is_active()) {
- return [
- 'status' => 'error',
- 'message' => 'This attempt is not in progress',
- ];
+ return ['status' => 'error', 'message' => 'This attempt is not in progress'];
}
if ($jazzquiz->is_instructor()) {
- return handle_instructor_request($action, $session);
+ return handle_instructor_request($action, $session, $jazzquiz);
} else {
- return handle_student_request($action, $session);
+ return handle_student_request($action, $session, $jazzquiz);
}
}
-$starttime = microtime(true);
$data = jazzquiz_ajax();
-$endtime = microtime(true);
-$data['debugmu'] = $endtime - $starttime;
echo json_encode($data);
diff --git a/amd/build/core.min.js b/amd/build/core.min.js
index 11df13c..611efb0 100644
--- a/amd/build/core.min.js
+++ b/amd/build/core.min.js
@@ -5,6 +5,6 @@ define("mod_jazzquiz/core",["exports","jquery","core/str","core_filters/events",
* @copyright 2014 University of Wisconsin - Madison
* @copyright 2018 NTNU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initialize=function(courseModuleId,activityId,sessionId,attemptId,sessionKey){session.courseModuleId=courseModuleId,session.activityId=activityId,session.sessionId=sessionId,session.attemptId=attemptId,session.sessionKey=sessionKey},_exports.setText=setText,_jquery=_interopRequireDefault(_jquery),_str=_interopRequireDefault(_str),_events=_interopRequireDefault(_events),_selectors=_interopRequireDefault(_selectors),_question=_interopRequireDefault(_question);let session={courseModuleId:0,activityId:0,sessionId:0,attemptId:0,sessionKey:""},cache=[];class Ajax{static request(method,url,data,success){return data.id=session.courseModuleId,data.sessionid=session.sessionId,data.attemptid=session.attemptId,data.sesskey=session.sessionKey,_jquery.default.ajax({type:method,url:url,data:data,dataType:"json",success:success}).fail((()=>setText(Quiz.info,"error_with_request")))}static get(action,data,success){return data.action=action,Ajax.request("get","ajax.php",data,success)}static post(action,data,success){return data.action=action,Ajax.request("post","ajax.php",data,success)}}class Quiz{constructor(Role){this.state="",this.isNewState=!1,this.question=new _question.default(this),this.role=new Role(this),this.events={notrunning:"onNotRunning",preparing:"onPreparing",running:"onRunning",reviewing:"onReviewing",sessionclosed:"onSessionClosed",voting:"onVoting"}}changeQuizState(state,data){this.isNewState=this.state!==state,this.state=state,void 0!==this.role.onStateChange&&this.role.onStateChange();const event=this.events[state];this.role[event](data)}poll(ms){Ajax.get("info",{},(data=>{this.changeQuizState(data.status,data),setTimeout((()=>this.poll(ms)),ms)}))}static get main(){return document.querySelector(_selectors.default.main)}static get info(){return document.querySelector(_selectors.default.quiz.info)}static get responded(){return document.querySelector(_selectors.default.quiz.responded)}static get responses(){return document.querySelector(_selectors.default.quiz.responses)}static get responseInfo(){return document.querySelector(_selectors.default.quiz.responseInfo)}static hide($element){$element.addClass("hidden")}static show($element){$element.removeClass("hidden")}static uncheck($element){$element.children(".fa").removeClass("fa-check-square-o").addClass("fa-square-o")}static check($element){$element.children(".fa").removeClass("fa-square-o").addClass("fa-check-square-o")}static renderAllMathjax(){_events.default.notifyFilterContentUpdated(document.getElementsByClassName("jazzquiz-response-container"))}static addMathjaxElement($target,latex){$target.html(''+latex+" "),Quiz.renderAllMathjax()}static renderMaximaEquation(input,targetId){null!==document.getElementById(targetId)&&(void 0===cache[input]?Ajax.get("stack",{input:encodeURIComponent(input)},(data=>{cache[data.original]=data.latex,Quiz.addMathjaxElement((0,_jquery.default)("#"+targetId),data.latex)})):Quiz.addMathjaxElement((0,_jquery.default)("#"+targetId),cache[input]))}}function setText($element,key,from,args){from=void 0!==from?from:"jazzquiz",args=void 0!==args?args:[],_jquery.default.when(_str.default.get_string(key,from,args)).done((text=>Quiz.show($element.html(text))))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Quiz=_exports.Ajax=void 0,_exports.initialize=function(courseModuleId,activityId,sessionId,attemptId,sessionKey){session.courseModuleId=courseModuleId,session.activityId=activityId,session.sessionId=sessionId,session.attemptId=attemptId,session.sessionKey=sessionKey},_exports.setText=setText,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors);let session={courseModuleId:0,activityId:0,sessionId:0,attemptId:0,sessionKey:""},cache=[];class Ajax{static request(method,url,data,success){return data.id=session.courseModuleId,data.sessionid=session.sessionId,data.attemptid=session.attemptId,data.sesskey=session.sessionKey,_jquery.default.ajax({type:method,url:url,data:data,dataType:"json",success:success}).fail((()=>setText(document.querySelector(_selectors.default.quiz.info),"error_with_request")))}static get(action,data,success){return data.action=action,Ajax.request("get","ajax.php",data,success)}static post(action,data,success){return data.action=action,Ajax.request("post","ajax.php",data,success)}}_exports.Ajax=Ajax;class Quiz{constructor(Role){this.state="",this.isNewState=!1,this.question=new _question.Question(this),this.role=new Role(this),this.events={notrunning:"onNotRunning",preparing:"onPreparing",running:"onRunning",reviewing:"onReviewing",sessionclosed:"onSessionClosed",voting:"onVoting"}}changeQuizState(state,data){this.isNewState=this.state!==state,this.state=state,void 0!==this.role.onStateChange&&this.role.onStateChange();const event=this.events[state];this.role[event](data)}poll(ms){Ajax.get("info",{},(data=>{this.changeQuizState(data.status,data),setTimeout((()=>this.poll(ms)),ms)}))}static renderAllMathjax(){(0,_events.notifyFilterContentUpdated)(document.getElementsByClassName("jazzquiz-response-container"))}static addMathjaxElement(target,latex){target.innerHTML=''+latex+" ",Quiz.renderAllMathjax()}static renderMaximaEquation(input,targetId){null!==document.getElementById(targetId)&&(void 0===cache[input]?Ajax.get("stack",{input:encodeURIComponent(input)},(data=>{cache[data.original]=data.latex,Quiz.addMathjaxElement(document.getElementById(targetId),data.latex)})):Quiz.addMathjaxElement(document.getElementById(targetId),cache[input]))}}function setText(element,key,from,args){from=void 0!==from?from:"jazzquiz",args=void 0!==args?args:[],_jquery.default.when((0,_str.get_string)(key,from,args)).done((text=>{element.innerHTML=text,element.classList.remove("hidden")}))}_exports.Quiz=Quiz}));
//# sourceMappingURL=core.min.js.map
\ No newline at end of file
diff --git a/amd/build/core.min.js.map b/amd/build/core.min.js.map
index 6c8f45e..c46a67e 100644
--- a/amd/build/core.min.js.map
+++ b/amd/build/core.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"core.min.js","sources":["../src/core.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport mString from 'core/str';\nimport mEvent from 'core_filters/events';\nimport selectors from 'mod_jazzquiz/selectors';\nimport Question from \"mod_jazzquiz/question\";\n\n// Contains the needed values for using the ajax script.\nlet session = {\n courseModuleId: 0,\n activityId: 0, // TODO: Remove activityId? Unsure if used.\n sessionId: 0,\n attemptId: 0,\n sessionKey: ''\n};\n\n// Used for caching the latex of maxima input.\nlet cache = [];\n\n// TODO: Migrate to core/ajax module?\nclass Ajax {\n\n /**\n * Send a request using AJAX, with method specified.\n *\n * @param {string} method Which HTTP method to use.\n * @param {string} url Relative to root of jazzquiz module. Does not start with /.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static request(method, url, data, success) {\n data.id = session.courseModuleId;\n data.sessionid = session.sessionId;\n data.attemptid = session.attemptId;\n data.sesskey = session.sessionKey;\n return $.ajax({\n type: method,\n url: url,\n data: data,\n dataType: 'json',\n success: success\n }).fail(() => setText(Quiz.info, 'error_with_request'));\n }\n\n /**\n * Send a GET request using AJAX.\n * @param {string} action Which action to query.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static get(action, data, success) {\n data.action = action;\n return Ajax.request('get', 'ajax.php', data, success);\n }\n\n /**\n * Send a POST request using AJAX.\n * @param {string} action Which action to query.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static post(action, data, success) {\n data.action = action;\n return Ajax.request('post', 'ajax.php', data, success);\n }\n\n}\n\nclass Quiz {\n\n constructor(Role) {\n this.state = '';\n this.isNewState = false;\n this.question = new Question(this);\n this.role = new Role(this);\n this.events = {\n notrunning: 'onNotRunning',\n preparing: 'onPreparing',\n running: 'onRunning',\n reviewing: 'onReviewing',\n sessionclosed: 'onSessionClosed',\n voting: 'onVoting'\n };\n }\n\n changeQuizState(state, data) {\n this.isNewState = (this.state !== state);\n this.state = state;\n if (this.role.onStateChange !== undefined) {\n this.role.onStateChange();\n }\n const event = this.events[state];\n this.role[event](data);\n }\n\n /**\n * Initiate the chained session info calls to ajax.php\n * @param {number} ms interval in milliseconds\n */\n poll(ms) {\n Ajax.get('info', {}, data => {\n this.changeQuizState(data.status, data);\n setTimeout(() => this.poll(ms), ms);\n });\n }\n\n static get main() {\n return document.querySelector(selectors.main);\n }\n\n static get info() {\n return document.querySelector(selectors.quiz.info);\n }\n\n static get responded() {\n return document.querySelector(selectors.quiz.responded);\n }\n\n static get responses() {\n return document.querySelector(selectors.quiz.responses);\n }\n\n static get responseInfo() {\n return document.querySelector(selectors.quiz.responseInfo);\n }\n\n static hide($element) {\n $element.addClass('hidden');\n }\n\n static show($element) {\n $element.removeClass('hidden');\n }\n\n static uncheck($element) {\n $element.children('.fa').removeClass('fa-check-square-o').addClass('fa-square-o');\n }\n\n static check($element) {\n $element.children('.fa').removeClass('fa-square-o').addClass('fa-check-square-o');\n }\n\n /**\n * Triggers a dynamic content update event, which MathJax listens to.\n */\n static renderAllMathjax() {\n mEvent.notifyFilterContentUpdated(document.getElementsByClassName('jazzquiz-response-container'));\n }\n\n /**\n * Sets the body of the target, and triggers an event letting MathJax know about the element.\n * @param {*} $target\n * @param {string} latex\n */\n static addMathjaxElement($target, latex) {\n $target.html('' + latex + ' ');\n Quiz.renderAllMathjax();\n }\n\n /**\n * Converts the input to LaTeX and renders it to the target with MathJax.\n * @param {string} input\n * @param {string} targetId\n */\n static renderMaximaEquation(input, targetId) {\n const target = document.getElementById(targetId);\n if (target === null) {\n // Log error to console: 'Target element #' + targetId + ' not found.'.\n return;\n }\n if (cache[input] !== undefined) {\n Quiz.addMathjaxElement($('#' + targetId), cache[input]);\n return;\n }\n Ajax.get('stack', {input: encodeURIComponent(input)}, data => {\n cache[data.original] = data.latex;\n Quiz.addMathjaxElement($('#' + targetId), data.latex);\n });\n }\n\n}\n\n/**\n * Retrieve a language string that was sent along with the page.\n * @param {*} $element\n * @param {string} key Which string in the language file we want.\n * @param {string} [from=jazzquiz] Which language file we want the string from. Default is jazzquiz.\n * @param {array} [args=[]] This is {$a} in the string for the key.\n */\nexport function setText($element, key, from, args) {\n from = (from !== undefined) ? from : 'jazzquiz';\n args = (args !== undefined) ? args : [];\n $.when(mString.get_string(key, from, args))\n .done(text => Quiz.show($element.html(text)));\n}\n\n/**\n * Initialize session data.\n *\n * @param {number} courseModuleId\n * @param {number} activityId\n * @param {number} sessionId\n * @param {number} attemptId\n * @param {string} sessionKey\n */\nexport function initialize(courseModuleId, activityId, sessionId, attemptId, sessionKey) {\n session.courseModuleId = courseModuleId;\n session.activityId = activityId;\n session.sessionId = sessionId;\n session.attemptId = attemptId;\n session.sessionKey = sessionKey;\n}\n"],"names":["courseModuleId","activityId","sessionId","attemptId","sessionKey","session","cache","Ajax","method","url","data","success","id","sessionid","attemptid","sesskey","$","ajax","type","dataType","fail","setText","Quiz","info","action","request","constructor","Role","state","isNewState","question","Question","this","role","events","notrunning","preparing","running","reviewing","sessionclosed","voting","changeQuizState","undefined","onStateChange","event","poll","ms","get","status","setTimeout","main","document","querySelector","selectors","quiz","responded","responses","responseInfo","$element","addClass","removeClass","children","notifyFilterContentUpdated","getElementsByClassName","$target","latex","html","renderAllMathjax","input","targetId","getElementById","encodeURIComponent","original","addMathjaxElement","key","from","args","when","mString","get_string","done","text","show"],"mappings":";;;;;;;0FAsO2BA,eAAgBC,WAAYC,UAAWC,UAAWC,YACzEC,QAAQL,eAAiBA,eACzBK,QAAQJ,WAAaA,WACrBI,QAAQH,UAAYA,UACpBG,QAAQF,UAAYA,UACpBE,QAAQD,WAAaA,qPA7MrBC,QAAU,CACVL,eAAgB,EAChBC,WAAY,EACZC,UAAW,EACXC,UAAW,EACXC,WAAY,IAIZE,MAAQ,SAGNC,oBAWaC,OAAQC,IAAKC,KAAMC,gBAC9BD,KAAKE,GAAKP,QAAQL,eAClBU,KAAKG,UAAYR,QAAQH,UACzBQ,KAAKI,UAAYT,QAAQF,UACzBO,KAAKK,QAAUV,QAAQD,WAChBY,gBAAEC,KAAK,CACVC,KAAMV,OACNC,IAAKA,IACLC,KAAMA,KACNS,SAAU,OACVR,QAASA,UACVS,MAAK,IAAMC,QAAQC,KAAKC,KAAM,mCAU1BC,OAAQd,KAAMC,gBACrBD,KAAKc,OAASA,OACPjB,KAAKkB,QAAQ,MAAO,WAAYf,KAAMC,qBAUrCa,OAAQd,KAAMC,gBACtBD,KAAKc,OAASA,OACPjB,KAAKkB,QAAQ,OAAQ,WAAYf,KAAMC,gBAKhDW,KAEFI,YAAYC,WACHC,MAAQ,QACRC,YAAa,OACbC,SAAW,IAAIC,kBAASC,WACxBC,KAAO,IAAIN,KAAKK,WAChBE,OAAS,CACVC,WAAY,eACZC,UAAW,cACXC,QAAS,YACTC,UAAW,cACXC,cAAe,kBACfC,OAAQ,YAIhBC,gBAAgBb,MAAOlB,WACdmB,WAAcG,KAAKJ,QAAUA,WAC7BA,MAAQA,WACmBc,IAA5BV,KAAKC,KAAKU,oBACLV,KAAKU,sBAERC,MAAQZ,KAAKE,OAAON,YACrBK,KAAKW,OAAOlC,MAOrBmC,KAAKC,IACDvC,KAAKwC,IAAI,OAAQ,IAAIrC,YACZ+B,gBAAgB/B,KAAKsC,OAAQtC,MAClCuC,YAAW,IAAMjB,KAAKa,KAAKC,KAAKA,OAI7BI,yBACAC,SAASC,cAAcC,mBAAUH,MAGjC3B,yBACA4B,SAASC,cAAcC,mBAAUC,KAAK/B,MAGtCgC,8BACAJ,SAASC,cAAcC,mBAAUC,KAAKC,WAGtCC,8BACAL,SAASC,cAAcC,mBAAUC,KAAKE,WAGtCC,iCACAN,SAASC,cAAcC,mBAAUC,KAAKG,0BAGrCC,UACRA,SAASC,SAAS,sBAGVD,UACRA,SAASE,YAAY,yBAGVF,UACXA,SAASG,SAAS,OAAOD,YAAY,qBAAqBD,SAAS,4BAG1DD,UACTA,SAASG,SAAS,OAAOD,YAAY,eAAeD,SAAS,+DAOtDG,2BAA2BX,SAASY,uBAAuB,yDAQ7CC,QAASC,OAC9BD,QAAQE,KAAK,+CAAiDD,MAAQ,WACtE3C,KAAK6C,+CAQmBC,MAAOC,UAEhB,OADAlB,SAASmB,eAAeD,iBAKlB3B,IAAjBpC,MAAM8D,OAIV7D,KAAKwC,IAAI,QAAS,CAACqB,MAAOG,mBAAmBH,SAAS1D,OAClDJ,MAAMI,KAAK8D,UAAY9D,KAAKuD,MAC5B3C,KAAKmD,mBAAkB,mBAAE,IAAMJ,UAAW3D,KAAKuD,UAL/C3C,KAAKmD,mBAAkB,mBAAE,IAAMJ,UAAW/D,MAAM8D,mBAkB5C/C,QAAQqC,SAAUgB,IAAKC,KAAMC,MACzCD,UAAiBjC,IAATiC,KAAsBA,KAAO,WACrCC,UAAiBlC,IAATkC,KAAsBA,KAAO,mBACnCC,KAAKC,aAAQC,WAAWL,IAAKC,KAAMC,OAChCI,MAAKC,MAAQ3D,KAAK4D,KAAKxB,SAASQ,KAAKe"}
\ No newline at end of file
+{"version":3,"file":"core.min.js","sources":["../src/core.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {get_string} from 'core/str';\nimport {notifyFilterContentUpdated} from 'core_filters/events';\nimport selectors from 'mod_jazzquiz/selectors';\nimport {Question} from 'mod_jazzquiz/question';\n\n// Contains the needed values for using the ajax script.\nlet session = {\n courseModuleId: 0,\n activityId: 0, // TODO: Remove activityId? Unsure if used.\n sessionId: 0,\n attemptId: 0,\n sessionKey: ''\n};\n\n// Used for caching the latex of maxima input.\nlet cache = [];\n\n// TODO: Migrate to core/ajax module?\nexport class Ajax {\n\n /**\n * Send a request using AJAX, with method specified.\n *\n * @param {string} method Which HTTP method to use.\n * @param {string} url Relative to root of jazzquiz module. Does not start with /.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static request(method, url, data, success) {\n data.id = session.courseModuleId;\n data.sessionid = session.sessionId;\n data.attemptid = session.attemptId;\n data.sesskey = session.sessionKey;\n return $.ajax({\n type: method,\n url: url,\n data: data,\n dataType: 'json',\n success: success\n }).fail(() => setText(document.querySelector(selectors.quiz.info), 'error_with_request'));\n }\n\n /**\n * Send a GET request using AJAX.\n * @param {string} action Which action to query.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static get(action, data, success) {\n data.action = action;\n return Ajax.request('get', 'ajax.php', data, success);\n }\n\n /**\n * Send a POST request using AJAX.\n * @param {string} action Which action to query.\n * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey\n * @param {function} success Callback function for when the request was completed successfully.\n * @return {jqXHR} The jQuery XHR object\n */\n static post(action, data, success) {\n data.action = action;\n return Ajax.request('post', 'ajax.php', data, success);\n }\n\n}\n\nexport class Quiz {\n\n constructor(Role) {\n this.state = '';\n this.isNewState = false;\n this.question = new Question(this);\n this.role = new Role(this);\n this.events = {\n notrunning: 'onNotRunning',\n preparing: 'onPreparing',\n running: 'onRunning',\n reviewing: 'onReviewing',\n sessionclosed: 'onSessionClosed',\n voting: 'onVoting'\n };\n }\n\n changeQuizState(state, data) {\n this.isNewState = (this.state !== state);\n this.state = state;\n if (this.role.onStateChange !== undefined) {\n this.role.onStateChange();\n }\n const event = this.events[state];\n this.role[event](data);\n }\n\n /**\n * Initiate the chained session info calls to ajax.php\n * @param {number} ms interval in milliseconds\n */\n poll(ms) {\n Ajax.get('info', {}, data => {\n this.changeQuizState(data.status, data);\n setTimeout(() => this.poll(ms), ms);\n });\n }\n\n /**\n * Triggers a dynamic content update event, which MathJax listens to.\n */\n static renderAllMathjax() {\n notifyFilterContentUpdated(document.getElementsByClassName('jazzquiz-response-container'));\n }\n\n /**\n * Sets the body of the target, and triggers an event letting MathJax know about the element.\n * @param {*} target\n * @param {string} latex\n */\n static addMathjaxElement(target, latex) {\n target.innerHTML = '' + latex + ' ';\n Quiz.renderAllMathjax();\n }\n\n /**\n * Converts the input to LaTeX and renders it to the target with MathJax.\n * @param {string} input\n * @param {string} targetId\n */\n static renderMaximaEquation(input, targetId) {\n const target = document.getElementById(targetId);\n if (target === null) {\n return;\n }\n if (cache[input] !== undefined) {\n Quiz.addMathjaxElement(document.getElementById(targetId), cache[input]);\n return;\n }\n Ajax.get('stack', {input: encodeURIComponent(input)}, data => {\n cache[data.original] = data.latex;\n Quiz.addMathjaxElement(document.getElementById(targetId), data.latex);\n });\n }\n\n}\n\n/**\n * Retrieve a language string that was sent along with the page.\n * @param {*} element\n * @param {string} key Which string in the language file we want.\n * @param {string} [from=jazzquiz] Which language file we want the string from. Default is jazzquiz.\n * @param {array} [args=[]] This is {$a} in the string for the key.\n */\nexport function setText(element, key, from, args) {\n from = (from !== undefined) ? from : 'jazzquiz';\n args = (args !== undefined) ? args : [];\n $.when(get_string(key, from, args)).done(text => {\n element.innerHTML = text;\n element.classList.remove('hidden');\n });\n}\n\n/**\n * Initialize session data.\n *\n * @param {number} courseModuleId\n * @param {number} activityId\n * @param {number} sessionId\n * @param {number} attemptId\n * @param {string} sessionKey\n */\nexport function initialize(courseModuleId, activityId, sessionId, attemptId, sessionKey) {\n session.courseModuleId = courseModuleId;\n session.activityId = activityId;\n session.sessionId = sessionId;\n session.attemptId = attemptId;\n session.sessionKey = sessionKey;\n}\n"],"names":["courseModuleId","activityId","sessionId","attemptId","sessionKey","session","cache","Ajax","method","url","data","success","id","sessionid","attemptid","sesskey","$","ajax","type","dataType","fail","setText","document","querySelector","selectors","quiz","info","action","request","Quiz","constructor","Role","state","isNewState","question","Question","this","role","events","notrunning","preparing","running","reviewing","sessionclosed","voting","changeQuizState","undefined","onStateChange","event","poll","ms","get","status","setTimeout","getElementsByClassName","target","latex","innerHTML","renderAllMathjax","input","targetId","getElementById","encodeURIComponent","original","addMathjaxElement","element","key","from","args","when","done","text","classList","remove"],"mappings":";;;;;;;6HAmM2BA,eAAgBC,WAAYC,UAAWC,UAAWC,YACzEC,QAAQL,eAAiBA,eACzBK,QAAQJ,WAAaA,WACrBI,QAAQH,UAAYA,UACpBG,QAAQF,UAAYA,UACpBE,QAAQD,WAAaA,+HA1KrBC,QAAU,CACVL,eAAgB,EAChBC,WAAY,EACZC,UAAW,EACXC,UAAW,EACXC,WAAY,IAIZE,MAAQ,SAGCC,oBAWMC,OAAQC,IAAKC,KAAMC,gBAC9BD,KAAKE,GAAKP,QAAQL,eAClBU,KAAKG,UAAYR,QAAQH,UACzBQ,KAAKI,UAAYT,QAAQF,UACzBO,KAAKK,QAAUV,QAAQD,WAChBY,gBAAEC,KAAK,CACVC,KAAMV,OACNC,IAAKA,IACLC,KAAMA,KACNS,SAAU,OACVR,QAASA,UACVS,MAAK,IAAMC,QAAQC,SAASC,cAAcC,mBAAUC,KAAKC,MAAO,mCAU5DC,OAAQjB,KAAMC,gBACrBD,KAAKiB,OAASA,OACPpB,KAAKqB,QAAQ,MAAO,WAAYlB,KAAMC,qBAUrCgB,OAAQjB,KAAMC,gBACtBD,KAAKiB,OAASA,OACPpB,KAAKqB,QAAQ,OAAQ,WAAYlB,KAAMC,mCAKzCkB,KAETC,YAAYC,WACHC,MAAQ,QACRC,YAAa,OACbC,SAAW,IAAIC,mBAASC,WACxBC,KAAO,IAAIN,KAAKK,WAChBE,OAAS,CACVC,WAAY,eACZC,UAAW,cACXC,QAAS,YACTC,UAAW,cACXC,cAAe,kBACfC,OAAQ,YAIhBC,gBAAgBb,MAAOtB,WACduB,WAAcG,KAAKJ,QAAUA,WAC7BA,MAAQA,WACmBc,IAA5BV,KAAKC,KAAKU,oBACLV,KAAKU,sBAERC,MAAQZ,KAAKE,OAAON,YACrBK,KAAKW,OAAOtC,MAOrBuC,KAAKC,IACD3C,KAAK4C,IAAI,OAAQ,IAAIzC,YACZmC,gBAAgBnC,KAAK0C,OAAQ1C,MAClC2C,YAAW,IAAMjB,KAAKa,KAAKC,KAAKA,wEAQT5B,SAASgC,uBAAuB,yDAQtCC,OAAQC,OAC7BD,OAAOE,UAAY,+CAAiDD,MAAQ,UAC5E3B,KAAK6B,+CAQmBC,MAAOC,UAEhB,OADAtC,SAASuC,eAAeD,iBAIlBd,IAAjBxC,MAAMqD,OAIVpD,KAAK4C,IAAI,QAAS,CAACQ,MAAOG,mBAAmBH,SAASjD,OAClDJ,MAAMI,KAAKqD,UAAYrD,KAAK8C,MAC5B3B,KAAKmC,kBAAkB1C,SAASuC,eAAeD,UAAWlD,KAAK8C,UAL/D3B,KAAKmC,kBAAkB1C,SAASuC,eAAeD,UAAWtD,MAAMqD,mBAkB5DtC,QAAQ4C,QAASC,IAAKC,KAAMC,MACxCD,UAAiBrB,IAATqB,KAAsBA,KAAO,WACrCC,UAAiBtB,IAATsB,KAAsBA,KAAO,mBACnCC,MAAK,mBAAWH,IAAKC,KAAMC,OAAOE,MAAKC,OACrCN,QAAQR,UAAYc,KACpBN,QAAQO,UAAUC,OAAO"}
\ No newline at end of file
diff --git a/amd/build/instructor.min.js b/amd/build/instructor.min.js
index 55cd3de..b78c139 100644
--- a/amd/build/instructor.min.js
+++ b/amd/build/instructor.min.js
@@ -1,10 +1,10 @@
-define("mod_jazzquiz/instructor",["exports","jquery","mod_jazzquiz/core","mod_jazzquiz/selectors"],(function(_exports,_jquery,_core,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("mod_jazzquiz/instructor",["exports","jquery","mod_jazzquiz/core","mod_jazzquiz/question","mod_jazzquiz/selectors"],(function(_exports,_jquery,_core,_question,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* @module mod_jazzquiz
* @author Sebastian S. Gundersen
* @copyright 2014 University of Wisconsin - Madison
* @copyright 2018 NTNU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initialize=void 0,_jquery=_interopRequireDefault(_jquery),_core=_interopRequireDefault(_core),_selectors=_interopRequireDefault(_selectors);const Quiz=_core.default.Quiz,Question=_core.default.Question,Ajax=_core.default.Ajax,setText=_core.default.setText;class ResponseView{constructor(quiz){this.quiz=quiz,this.currentResponses=[],this.showVotesUponReview=!1,this.respondedCount=0,this.showResponses=!1,this.totalStudents=0,document.addEventListener("click",(event=>{event.target.closest(_selectors.default.quiz.undoMergeButton)&&this.undoMerge();event.target.closest(_selectors.default.quiz.showNormalResultsButton)&&this.refresh(!1);event.target.closest(_selectors.default.quiz.showVoteResultsButton)&&this.refreshVotes(),event.target.classList.contains("bar")?this.startMerge(event.target.id):event.target.parentNode&&event.target.parentNode.classList.contains("bar")&&this.startMerge(event.target.parentNode.id)}))}clear(){Quiz.responses.html(""),Quiz.responseInfo.html("")}hide(){Quiz.uncheck(Instructor.control("responses")),Quiz.hide(Quiz.responses),Quiz.hide(Quiz.responseInfo)}show(){Quiz.check(Instructor.control("responses")),Quiz.show(Quiz.responses),Quiz.show(Quiz.responseInfo),this.showVotesUponReview?(this.refreshVotes(),this.showVotesUponReview=!1):this.refresh(!1)}toggle(){this.showResponses=!this.showResponses,this.showResponses?this.show():this.hide()}endMerge(){document.querySelectorAll(_selectors.default.quiz.mergeInto).forEach((element=>element.classList.remove("merge-into"))),document.querySelectorAll(_selectors.default.quiz.mergeFrom).forEach((element=>element.classList.remove("merge-from")))}undoMerge(){Ajax.post("undo_merge",{},(()=>this.refresh(!0)))}merge(from,into){Ajax.post("merge_responses",{from:from,into:into},(()=>this.refresh(!1)))}startMerge(fromRowBarId){const barCell=document.getElementById(fromRowBarId),row=barCell.parentElement;if(row.classList.contains("merge-from"))this.endMerge();else{if(row.classList.contains("merge-into")){const fromRow=document.querySelector(_selectors.default.quiz.mergeFrom);return this.merge(fromRow.dataset.response,row.dataset.response),void this.endMerge()}row.classList.add("merge-from"),row.closest("table").querySelectorAll("tr").forEach((tableRow=>{const cell=tableRow.querySelector("td:nth-child(2)");cell&&cell.id!==barCell.id&&tableRow.classList.add("merge-into")}))}}createControls(name){if(this.quiz.question.hasVotes){if("reviewing"===this.quiz.state){let $showNormalResult=(0,_jquery.default)("#review_show_normal_results"),$showVoteResult=(0,_jquery.default)("#review_show_vote_results");Quiz.show(Quiz.responseInfo),"vote_response"===name?0===$showNormalResult.length&&(setText(Quiz.responseInfo.html(' ').children("h4"),"showing_vote_results"),Quiz.responseInfo.append(' '),setText((0,_jquery.default)("#review_show_normal_results"),"click_to_show_original_results"),$showVoteResult.remove()):"current_response"===name&&0===$showVoteResult.length&&(setText(Quiz.responseInfo.html(' ').children("h4"),"showing_original_results"),Quiz.responseInfo.append(' '),setText((0,_jquery.default)("#review_show_vote_results"),"click_to_show_vote_results"),$showNormalResult.remove())}}else Quiz.hide(Quiz.responseInfo)}addBarGraphRow(target,name,response,i,highestResponseCount){const percent=parseInt(response.count)/highestResponseCount*100;let rowIndex=-1,currentRowIndex=-1;for(let j=0;j'+response.count+"";let responseCell=row.insertCell(0);responseCell.onclick=function(){(0,_jquery.default)(this).parent().toggleClass("selected-vote-option")};let barCell=row.insertCell(1);barCell.classList.add("bar"),barCell.id=name+"_bar_"+rowIndex,barCell.innerHTML=''+countHtml+"
";const latexId=name+"_latex_"+rowIndex;responseCell.innerHTML=' ',Quiz.addMathjaxElement((0,_jquery.default)("#"+latexId),response.response),"stack"===response.qtype&&Quiz.renderMaximaEquation(response.response,latexId)}else{let currentRow=target.rows[currentRowIndex];currentRow.dataset.rowIndex=rowIndex,currentRow.dataset.responseIndex=i,currentRow.dataset.percent=percent,currentRow.dataset.count=response.count;const containsOutside=currentRow.classList.contains("outside");percent>15&&containsOutside?currentRow.classList.remove("outside"):percent<15&&!containsOutside&¤tRow.classList.add("outside");let countElement=document.getElementById(name+"_count_"+rowIndex);null!==countElement&&(countElement.innerHTML=response.count);let barElement=document.getElementById(name+"_bar_"+rowIndex);null!==barElement&&(barElement.firstElementChild.style.width=percent+"%")}}createBarGraph(responses,name,targetId,graphId,rebuild){let target=document.getElementById(targetId);if(null===target)return;let highestResponseCount=0;for(let i=0;ihighestResponseCount&&(highestResponseCount=count)}rebuild&&(target.innerHTML="");for(let i=0;i';Quiz.show((0,_jquery.default)("#"+wrapperId).html(html))}this.createBarGraph(this.currentResponses,"current_response",tableId,graphId,rebuild),ResponseView.sortBarGraph(tableId)}}refresh(rebuild){Ajax.get("get_results",{},(data=>{this.quiz.question.hasVotes=data.has_votes,this.totalStudents=parseInt(data.total_students),this.set("jazzquiz_responses_container","current_responses_wrapper",data.responses,data.responded,data.question_type,"results",rebuild),data.merge_count>0?Quiz.show((0,_jquery.default)("#jazzquiz_undo_merge")):Quiz.hide((0,_jquery.default)("#jazzquiz_undo_merge"))}))}refreshVotes(){if(!this.showResponses&&"reviewing"!==this.quiz.state)return Quiz.hide(Quiz.responseInfo),void Quiz.hide(Quiz.responses);Ajax.get("get_vote_results",{},(data=>{const answers=data.answers,targetId="wrapper_vote_responses";let responses=[];this.respondedCount=0,this.totalStudents=parseInt(data.total_students);for(let i in answers)answers.hasOwnProperty(i)&&(responses.push({response:answers[i].attempt,count:answers[i].finalcount,qtype:answers[i].qtype,slot:answers[i].slot}),this.respondedCount+=parseInt(answers[i].finalcount));if(setText(Quiz.responded.find("h4"),"a_out_of_b_voted","jazzquiz",{a:this.respondedCount,b:this.totalStudents}),null===document.getElementById(targetId)){const html='';Quiz.show(Quiz.responses.html(html))}this.createBarGraph(responses,"vote_response",targetId,"vote",!1),ResponseView.sortBarGraph(targetId)}))}}class Instructor{constructor(quiz){this.quiz=quiz,this.responses=new ResponseView(quiz),this.isShowingCorrectAnswer=!1,this.totalQuestions=0,this.allowVote=!1,(0,_jquery.default)(document).on("keyup",(event=>{27===event.keyCode&&Instructor.closeFullscreenView()})),(0,_jquery.default)(document).on("click",(event=>{Instructor.closeQuestionListMenu(event,"improvise"),Instructor.closeQuestionListMenu(event,"jump")})),Instructor.addEvents({repoll:()=>this.repollQuestion(),vote:()=>this.runVoting(),improvise:()=>this.showQuestionListSetup("improvise"),jump:()=>this.showQuestionListSetup("jump"),next:()=>this.nextQuestion(),random:()=>this.randomQuestion(),end:()=>this.endQuestion(),fullscreen:()=>Instructor.showFullscreenView(),answer:()=>this.showCorrectAnswer(),responses:()=>this.responses.toggle(),exit:()=>this.closeSession(),quit:()=>this.closeSession(),startquiz:()=>this.startQuiz()}),Instructor.addHotkeys({t:"responses",r:"repoll",a:"answer",e:"end",j:"jump",i:"improvise",v:"vote",n:"next",m:"random",f:"fullscreen"})}static addHotkeys(keys){for(let key in keys)keys.hasOwnProperty(key)&&(keys[key]={action:keys[key],repeat:!1},(0,_jquery.default)(document).on("keydown",(event=>{if(keys[key].repeat||event.ctrlKey)return;if(String.fromCharCode(event.which).toLowerCase()!==key)return;let focusedTag=(0,_jquery.default)(":focus").prop("tagName");if(void 0!==focusedTag&&(focusedTag=focusedTag.toLowerCase(),"input"===focusedTag||"textarea"===focusedTag))return;event.preventDefault(),keys[key].repeat=!0;let $control=Instructor.control(keys[key].action);$control.length&&!$control.prop("disabled")&&$control.click()})),(0,_jquery.default)(document).on("keyup",(event=>{String.fromCharCode(event.which).toLowerCase()===key&&(keys[key].repeat=!1)})))}static addEvents(events){for(let key in events)events.hasOwnProperty(key)&&(0,_jquery.default)(document).on("click","#jazzquiz_control_"+key,(()=>{Instructor.enableControls([]),events[key]()}))}static get controls(){return(0,_jquery.default)("#jazzquiz_controls_box")}static get controlButtons(){return Instructor.controls.find(".quiz-control-buttons")}static control(key){return(0,_jquery.default)("#jazzquiz_control_"+key)}static get side(){return(0,_jquery.default)("#jazzquiz_side_container")}static get correctAnswer(){return(0,_jquery.default)("#jazzquiz_correct_answer_container")}static get isMerging(){return 0!==(0,_jquery.default)(".merge-from").length}onNotRunning(data){this.responses.totalStudents=data.student_count,Quiz.hide(Instructor.side),setText(Quiz.info,"instructions_for_instructor"),Instructor.enableControls([]),Quiz.hide(Instructor.controlButtons);let $studentsJoined=Instructor.control("startquiz").next();1===data.student_count?setText($studentsJoined,"one_student_has_joined"):data.student_count>1?setText($studentsJoined,"x_students_have_joined","jazzquiz",data.student_count):setText($studentsJoined,"no_students_have_joined"),Quiz.show(Instructor.control("startquiz").parent())}onPreparing(data){Quiz.hide(Instructor.side),setText(Quiz.info,"instructions_for_instructor");let enabledButtons=["improvise","jump","random","fullscreen","quit"];data.slot0&&data.delay<-data.questiontime&&this.endQuestion(),this.responses.refresh(!Instructor.isMerging);else{this.quiz.question.startCountdown(data.questiontime,data.delay)&&(this.quiz.question.isRunning=!0)}}onReviewing(data){Quiz.show(Instructor.side);let enabledButtons=["answer","repoll","fullscreen","improvise","jump","random","quit"];this.allowVote&&enabledButtons.push("vote"),data.slot(0,_jquery.default)("#jazzquiz_controls").removeClass("btn-hide")))}endQuestion(){this.quiz.question.hideTimer(),Ajax.post("end_question",{},(()=>{"voting"===this.quiz.state?this.responses.showVotesUponReview=!0:(this.quiz.question.isRunning=!1,Instructor.enableControls([]))}))}showQuestionListSetup(name){let $controlButton=Instructor.control(name);$controlButton.hasClass("active")||Ajax.get("list_"+name+"_questions",{},(data=>{let self=this,$menu=(0,_jquery.default)("#jazzquiz_"+name+"_menu");const menuMargin=$controlButton.offset().left-$controlButton.parent().offset().left;$menu.html("").addClass("active").css("margin-left",menuMargin+"px"),$controlButton.addClass("active");const questions=data.questions;for(let i in questions){if(!questions.hasOwnProperty(i))continue;let $questionButton=(0,_jquery.default)(' ');Quiz.addMathjaxElement($questionButton,questions[i].name),$questionButton.data({time:questions[i].time,"question-id":questions[i].questionid,"jazzquiz-question-id":questions[i].jazzquizquestionid}),$questionButton.data("test",1),$questionButton.on("click",(function(){const questionId=(0,_jquery.default)(this).data("question-id"),time=(0,_jquery.default)(this).data("time"),jazzQuestionId=(0,_jquery.default)(this).data("jazzquiz-question-id");self.jumpQuestion(questionId,time,jazzQuestionId),$menu.html("").removeClass("active"),$controlButton.removeClass("active")})),$menu.append($questionButton)}}))}static getSelectedAnswersForVote(){let result=[];return(0,_jquery.default)(".selected-vote-option").each(((i,option)=>{result.push({text:option.dataset.response,count:option.dataset.count})})),result}runVoting(){const options=Instructor.getSelectedAnswersForVote(),data={questions:encodeURIComponent(JSON.stringify(options))};Ajax.post("run_voting",data)}startQuestion(method,questionId,questionTime,jazzquizQuestionId){Quiz.hide(Quiz.info),this.responses.clear(),this.hideCorrectAnswer(),Ajax.post("start_question",{method:method,questionid:questionId,questiontime:questionTime,jazzquizquestionid:jazzquizQuestionId},(data=>this.quiz.question.startCountdown(data.questiontime,data.delay)))}jumpQuestion(questionId,questionTime,jazzquizQuestionId){this.startQuestion("jump",questionId,questionTime,jazzquizQuestionId)}repollQuestion(){this.startQuestion("repoll",0,0,0)}nextQuestion(){this.startQuestion("next",0,0,0)}randomQuestion(){this.startQuestion("random",0,0,0)}closeSession(){Quiz.hide((0,_jquery.default)("#jazzquiz_undo_merge")),Quiz.hide(Question.box),Quiz.hide(Instructor.controls),setText(Quiz.info,"closing_session"),Ajax.post("close_session",{},(()=>window.location=location.href.split("&")[0]))}hideCorrectAnswer(){this.isShowingCorrectAnswer&&(Quiz.hide(Instructor.correctAnswer),Quiz.uncheck(Instructor.control("answer")),this.isShowingCorrectAnswer=!1)}showCorrectAnswer(){this.hideCorrectAnswer(),Ajax.get("get_right_response",{},(data=>{Quiz.show(Instructor.correctAnswer.html(data.right_answer)),Quiz.renderAllMathjax(),Quiz.check(Instructor.control("answer")),this.isShowingCorrectAnswer=!0}))}static enableControls(buttons){Instructor.controlButtons.children("button").each(((index,child)=>{const id=child.getAttribute("id").replace("jazzquiz_control_","");child.disabled=-1===buttons.indexOf(id)}))}static showFullscreenView(){Quiz.main.hasClass("jazzquiz-fullscreen")?Instructor.closeFullscreenView():(document.documentElement.style.overflowY="hidden",Quiz.main.addClass("jazzquiz-fullscreen"))}static closeFullscreenView(){document.documentElement.style.overflowY="auto",Quiz.main.removeClass("jazzquiz-fullscreen")}static closeQuestionListMenu(event,name){const menuId="#jazzquiz_"+name+"_menu";(0,_jquery.default)(event.target).closest(menuId).length||((0,_jquery.default)(menuId).html("").removeClass("active"),Instructor.control(name).removeClass("active"))}static addReportEventHandlers(){(0,_jquery.default)(document).on("click","#report_overview_controls button",(function(){const action=(0,_jquery.default)(this).data("action");"attendance"===action?((0,_jquery.default)("#report_overview_responded").fadeIn(),(0,_jquery.default)("#report_overview_responses").fadeOut()):"responses"===action&&((0,_jquery.default)("#report_overview_responses").fadeIn(),(0,_jquery.default)("#report_overview_responded").fadeOut())}))}}_exports.initialize=(totalQuestions,reportView,slots)=>{let quiz=new Quiz(Instructor);quiz.role.totalQuestions=totalQuestions,reportView?(Instructor.addReportEventHandlers(),quiz.role.responses.showResponses=!0,slots.forEach((slot=>{const wrapper="jazzquiz_wrapper_responses_"+slot.num,table="responses_wrapper_table_"+slot.num,graph="report_"+slot.num;quiz.role.responses.set(wrapper,table,slot.responses,void 0,slot.type,graph,!1)}))):quiz.poll(500)}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initialize=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors);class ResponseView{constructor(quiz){this.quiz=quiz,this.currentResponses=[],this.showVotesUponReview=!1,this.respondedCount=0,this.showResponses=!1,this.totalStudents=0,document.addEventListener("click",(event=>{event.target.closest(_selectors.default.quiz.undoMergeButton)&&this.undoMerge();event.target.closest(_selectors.default.quiz.showNormalResultsButton)&&this.refresh(!1);event.target.closest(_selectors.default.quiz.showVoteResultsButton)&&this.refreshVotes(),event.target.classList.contains("bar")?this.startMerge(event.target.id):event.target.parentNode&&event.target.parentNode.classList.contains("bar")&&this.startMerge(event.target.parentNode.id)}))}clear(){document.querySelector(_selectors.default.quiz.responses).innerHTML="",document.querySelector(_selectors.default.quiz.responseInfo).innerHTML=""}hide(){Instructor.control("responses").querySelector(".fa").classList.replace("fa-check-square-o","fa-square-o"),document.querySelector(_selectors.default.quiz.responses).classList.add("hidden"),document.querySelector(_selectors.default.quiz.responseInfo).classList.add("hidden")}show(){Instructor.control("responses").querySelector(".fa").classList.replace("fa-square-o","fa-check-square-o"),document.querySelector(_selectors.default.quiz.responses).classList.remove("hidden"),document.querySelector(_selectors.default.quiz.responseInfo).classList.remove("hidden"),this.showVotesUponReview?(this.refreshVotes(),this.showVotesUponReview=!1):this.refresh(!1)}toggle(){this.showResponses=!this.showResponses,this.showResponses?this.show():this.hide()}endMerge(){document.querySelectorAll(_selectors.default.quiz.mergeInto).forEach((element=>element.classList.remove("merge-into"))),document.querySelectorAll(_selectors.default.quiz.mergeFrom).forEach((element=>element.classList.remove("merge-from")))}undoMerge(){_core.Ajax.post("undo_merge",{},(()=>this.refresh(!0)))}merge(from,into){_core.Ajax.post("merge_responses",{from:from,into:into},(()=>this.refresh(!1)))}startMerge(fromRowBarId){const barCell=document.getElementById(fromRowBarId),row=barCell.parentElement;if(row.classList.contains("merge-from"))this.endMerge();else{if(row.classList.contains("merge-into")){const fromRow=document.querySelector(_selectors.default.quiz.mergeFrom);return this.merge(fromRow.dataset.response,row.dataset.response),void this.endMerge()}row.classList.add("merge-from"),row.closest("table").querySelectorAll("tr").forEach((tableRow=>{const cell=tableRow.querySelector("td:nth-child(2)");cell&&cell.id!==barCell.id&&tableRow.classList.add("merge-into")}))}}createControls(name){const quizResponseInfo=document.querySelector(_selectors.default.quiz.responseInfo);if(this.quiz.question.hasVotes){if("reviewing"===this.quiz.state){let showNormalResult=document.querySelector(_selectors.default.quiz.showNormalResultsButton),showVoteResult=document.querySelector(_selectors.default.quiz.showVoteResultsButton);quizResponseInfo.classList.remove("hidden"),"vote_response"===name?showNormalResult||((0,_core.setText)(quizResponseInfo.html(' ').children("h4"),"showing_vote_results"),quizResponseInfo.innerHTML+=' ',showNormalResult=document.querySelector(_selectors.default.quiz.showNormalResultsButton),(0,_core.setText)(showNormalResult,"click_to_show_original_results"),showVoteResult.remove()):"current_response"===name&&(showVoteResult||(quizResponseInfo.innerHTML=' ',(0,_core.setText)(quizResponseInfo.querySelector("h4"),"showing_original_results"),quizResponseInfo.innerHTML+=' ',showVoteResult=document.querySelector(_selectors.default.quiz.showVoteResultsButton),(0,_core.setText)(showVoteResult,"click_to_show_vote_results"),showNormalResult.remove()))}}else quizResponseInfo.classList.add("hidden")}addBarGraphRow(target,name,response,i,highestResponseCount){const percent=parseInt(response.count)/highestResponseCount*100;let rowIndex=-1,currentRowIndex=-1;for(let j=0;j'+response.count+"";let responseCell=row.insertCell(0);responseCell.onclick=()=>responseCell.parentElement.classList.toggle("selected-vote-option");let barCell=row.insertCell(1);barCell.classList.add("bar"),barCell.id=name+"_bar_"+rowIndex,barCell.innerHTML=''+countHtml+"
";const latexId=name+"_latex_"+rowIndex;responseCell.innerHTML=' ',_core.Quiz.addMathjaxElement(document.getElementById(latexId),response.response),"stack"===response.qtype&&_core.Quiz.renderMaximaEquation(response.response,latexId)}else{let currentRow=target.rows[currentRowIndex];currentRow.dataset.rowIndex=rowIndex,currentRow.dataset.responseIndex=i,currentRow.dataset.percent=percent,currentRow.dataset.count=response.count;const containsOutside=currentRow.classList.contains("outside");percent>15&&containsOutside?currentRow.classList.remove("outside"):percent<15&&!containsOutside&¤tRow.classList.add("outside");let countElement=document.getElementById(name+"_count_"+rowIndex);null!==countElement&&(countElement.innerHTML=response.count);let barElement=document.getElementById(name+"_bar_"+rowIndex);null!==barElement&&(barElement.firstElementChild.style.width=percent+"%")}}createBarGraph(responses,name,targetId,graphId,rebuild){let target=document.getElementById(targetId);if(null===target)return;let highestResponseCount=0;for(let i=0;ihighestResponseCount&&(highestResponseCount=count)}rebuild&&(target.innerHTML="");for(let i=0;i'),wrapper.classList.remove("hidden")}this.createBarGraph(this.currentResponses,"current_response",tableId,graphId,rebuild),ResponseView.sortBarGraph(tableId)}refresh(rebuild){_core.Ajax.get("get_results",{},(data=>{this.quiz.question.hasVotes=data.has_votes,this.totalStudents=parseInt(data.total_students),this.set("jazzquiz_responses_container","current_responses_wrapper",data.responses,data.responded,data.question_type,"results",rebuild);const undoMergeButton=document.querySelector(_selectors.default.quiz.undoMergeButton);undoMergeButton&&undoMergeButton.classList.toggle("hidden",data.merge_count<=0)}))}refreshVotes(){const quizResponses=document.querySelector(_selectors.default.quiz.responses),quizResponseInfo=document.querySelector(_selectors.default.quiz.responseInfo);if(!this.showResponses&&"reviewing"!==this.quiz.state)return quizResponseInfo.classList.add("hidden"),void quizResponses.classList.add("hidden");_core.Ajax.get("get_vote_results",{},(data=>{const answers=data.answers,targetId="wrapper_vote_responses";let responses=[];this.respondedCount=0,this.totalStudents=parseInt(data.total_students);for(let i in answers)answers.hasOwnProperty(i)&&(responses.push({response:answers[i].attempt,count:answers[i].finalcount,qtype:answers[i].qtype,slot:answers[i].slot}),this.respondedCount+=parseInt(answers[i].finalcount));(0,_core.setText)(document.querySelector(_selectors.default.quiz.responded+" h4"),"a_out_of_b_voted","jazzquiz",{a:this.respondedCount,b:this.totalStudents}),null===document.getElementById(targetId)&&(quizResponses.innerHTML=''),quizResponses.classList.remove("hidden")),this.createBarGraph(responses,"vote_response",targetId,"vote",!1),ResponseView.sortBarGraph(targetId)}))}}class Instructor{constructor(quiz){this.quiz=quiz,this.responses=new ResponseView(quiz),this.isShowingCorrectAnswer=!1,this.totalQuestions=0,this.allowVote=!1,document.addEventListener("keyup",(event=>{"Escape"===event.key&&Instructor.closeFullscreenView()})),Instructor.addEvents({repoll:()=>this.repollQuestion(),vote:()=>this.runVoting(),improvise:()=>this.showQuestionListSetup("improvise"),jump:()=>this.showQuestionListSetup("jump"),next:()=>this.nextQuestion(),random:()=>this.randomQuestion(),end:()=>this.endQuestion(),fullscreen:()=>Instructor.showFullscreenView(),answer:()=>this.showCorrectAnswer(),responses:()=>this.responses.toggle(),exit:()=>this.closeSession(),quit:()=>this.closeSession(),startquiz:()=>this.startQuiz()}),Instructor.addHotkeys({t:"responses",r:"repoll",a:"answer",e:"end",j:"jump",i:"improvise",v:"vote",n:"next",m:"random",f:"fullscreen"}),document.addEventListener("click",(event=>{Instructor.closeQuestionListMenu(event,"improvise"),Instructor.closeQuestionListMenu(event,"jump"),event.target.closest(_selectors.default.quiz.startQuizButton)&&this.startQuiz(),event.target.closest(_selectors.default.quiz.exitQuizButton)&&this.closeSession()}))}static addHotkeys(keys){for(let key in keys)keys.hasOwnProperty(key)&&(keys[key]={action:keys[key],repeat:!1},document.addEventListener("keydown",(event=>{if(keys[key].repeat||event.ctrlKey)return;if(event.key.toLowerCase()!==key)return;let focusedTag=(0,_jquery.default)(":focus").prop("tagName");if(void 0!==focusedTag&&(focusedTag=focusedTag.toLowerCase(),"input"===focusedTag||"textarea"===focusedTag))return;event.preventDefault(),keys[key].repeat=!0;let $control=Instructor.control(keys[key].action);$control.length&&!$control.prop("disabled")&&$control.click()})),document.addEventListener("keyup",(event=>{event.key.toLowerCase()===key&&(keys[key].repeat=!1)})))}static addEvents(events){document.addEventListener("click",(event=>{const controlButton=event.target.closest(_selectors.default.quiz.controlButton);controlButton&&(Instructor.enableControls([]),events[controlButton.dataset.control]())}))}static get controls(){return document.querySelector(_selectors.default.quiz.controlsBox)}static get controlButtons(){return document.querySelector(_selectors.default.quiz.controlButtons)}static control(key){return document.getElementById("jazzquiz_control_".concat(key))}static get side(){return document.querySelector(_selectors.default.quiz.sideContainer)}static get correctAnswer(){return document.querySelector(_selectors.default.quiz.correctAnswerContainer)}static get isMerging(){return 0!==(0,_jquery.default)(".merge-from").length}onNotRunning(data){this.responses.totalStudents=data.student_count,Instructor.side.classList.add("hidden"),(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"instructions_for_instructor"),Instructor.enableControls([]),document.querySelector(_selectors.default.quiz.controlButtons).classList.add("hidden");const studentsJoined=document.querySelector(_selectors.default.quiz.studentsJoinedCounter);1===data.student_count?(0,_core.setText)(studentsJoined,"one_student_has_joined"):data.student_count>1?(0,_core.setText)(studentsJoined,"x_students_have_joined","jazzquiz",data.student_count):(0,_core.setText)(studentsJoined,"no_students_have_joined");Instructor.control("startquiz").parentElement.classList.remove("hidden")}onPreparing(data){Instructor.side.classList.add("hidden"),(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"instructions_for_instructor");let enabledButtons=["improvise","jump","random","fullscreen","quit"];data.slot0&&data.delay<-data.questiontime&&this.endQuestion(),this.responses.refresh(!Instructor.isMerging);else{this.quiz.question.startCountdown(data.questiontime,data.delay)&&(this.quiz.question.isRunning=!0)}}onReviewing(data){Instructor.side.classList.remove("hidden");let enabledButtons=["answer","repoll","fullscreen","improvise","jump","random","quit"];this.allowVote&&enabledButtons.push("vote"),data.slot{const controls=document.querySelector(_selectors.default.quiz.controls);controls&&controls.classList.remove("btn-hide")}))}endQuestion(){this.quiz.question.hideTimer(),_core.Ajax.post("end_question",{},(()=>{"voting"===this.quiz.state?this.responses.showVotesUponReview=!0:(this.quiz.question.isRunning=!1,Instructor.enableControls([]))}))}showQuestionListSetup(name){let controlButton=Instructor.control(name);controlButton.classList.contains("active")||_core.Ajax.get("list_".concat(name,"_questions"),{},(data=>{controlButton.classList.add("active");const menu=document.querySelector("#jazzquiz_".concat(name,"_menu"));menu.innerHTML="",menu.classList.add("active");const margin=controlButton.getBoundingClientRect().left-controlButton.parentElement.getBoundingClientRect().left;menu.style.marginLeft=margin+"px";const questions=data.questions;for(let i in questions){if(!questions.hasOwnProperty(i))continue;let questionButton=document.createElement("BUTTON");questionButton.classList.add("btn"),_core.Quiz.addMathjaxElement(questionButton,questions[i].name),questionButton.dataset.time=questions[i].time,questionButton.dataset.questionId=questions[i].questionid,questionButton.dataset.jazzquizQuestionId=questions[i].jazzquizquestionid,questionButton.addEventListener("click",(()=>{const questionId=questionButton.dataset.questionId,time=questionButton.dataset.time,jazzQuestionId=questionButton.dataset.jazzquizQuestionId;this.jumpQuestion(questionId,time,jazzQuestionId),menu.innerHTML="",menu.classList.remove("active"),controlButton.classList.remove("active")})),menu.appendChild(questionButton)}}))}static getSelectedAnswersForVote(){return document.querySelectorAll(_selectors.default.quiz.selectedVoteOption).map((option=>({text:option.dataset.response,count:option.dataset.count})))}runVoting(){const options=Instructor.getSelectedAnswersForVote(),data={questions:encodeURIComponent(JSON.stringify(options))};_core.Ajax.post("run_voting",data)}startQuestion(method,questionId,questionTime,jazzquizQuestionId){document.querySelector(_selectors.default.quiz.info).classList.add("hidden"),this.responses.clear(),this.hideCorrectAnswer(),_core.Ajax.post("start_question",{method:method,questionid:questionId,questiontime:questionTime,jazzquizquestionid:jazzquizQuestionId},(data=>this.quiz.question.startCountdown(data.questiontime,data.delay)))}jumpQuestion(questionId,questionTime,jazzquizQuestionId){this.startQuestion("jump",questionId,questionTime,jazzquizQuestionId)}repollQuestion(){this.startQuestion("repoll",0,0,0)}nextQuestion(){this.startQuestion("next",0,0,0)}randomQuestion(){this.startQuestion("random",0,0,0)}closeSession(){document.querySelector(_selectors.default.quiz.undoMergeButton).classList.add("hidden"),document.querySelector(_selectors.default.question.box).classList.add("hidden"),Instructor.controls.classList.add("hidden"),(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"closing_session"),_core.Ajax.post("close_session",{},(()=>window.location=location.href.split("&")[0]))}hideCorrectAnswer(){this.isShowingCorrectAnswer&&(Instructor.correctAnswer.classList.add("hidden"),Instructor.control("answer").querySelector(".fa").classList.replace("fa-check-square-o","fa-square-o"),this.isShowingCorrectAnswer=!1)}showCorrectAnswer(){this.hideCorrectAnswer(),_core.Ajax.get("get_right_response",{},(data=>{Instructor.correctAnswer.innerHTML=data.right_answer,Instructor.correctAnswer.classList.remove("hidden"),_core.Quiz.renderAllMathjax(),Instructor.control("answer").querySelector(".fa").classList.replace("fa-square-o","fa-check-square-o"),this.isShowingCorrectAnswer=!0}))}static enableControls(buttons){document.querySelectorAll(_selectors.default.quiz.controlButton).forEach((controlButton=>{controlButton.disabled=-1===buttons.indexOf(controlButton.dataset.control)}))}static showFullscreenView(){document.querySelector(_selectors.default.main).classList.contains("jazzquiz-fullscreen")?Instructor.closeFullscreenView():(document.documentElement.style.overflowY="hidden",document.querySelector(_selectors.default.main).classList.add("jazzquiz-fullscreen"))}static closeFullscreenView(){document.documentElement.style.overflowY="auto",document.querySelector(_selectors.default.main).classList.remove("jazzquiz-fullscreen")}static closeQuestionListMenu(event,name){if(!event.target.closest("#jazzquiz_".concat(name,"_menu"))){const menu=document.querySelector("#jazzquiz_".concat(name,"_menu"));menu.innerHTML="",menu.classList.remove("active");const controlButton=document.querySelector("#jazzquiz_control_".concat(name));controlButton&&controlButton.classList.remove("active")}}static addReportEventHandlers(){document.addEventListener("click",(event=>{const reportOverviewControlsButton=event.target.closest("#report_overview_controls button");if(reportOverviewControlsButton){const action=reportOverviewControlsButton.dataset.action;"attendance"===action?(document.querySelector(_selectors.default.quiz.reportOverviewResponded).classList.remove("hidden"),document.querySelector(_selectors.default.quiz.reportOverviewResponses).classList.add("hidden")):"responses"===action&&(document.querySelector(_selectors.default.quiz.reportOverviewResponses).classList.remove("hidden"),document.querySelector(_selectors.default.quiz.reportOverviewResponded).classList.add("hidden"))}}))}}_exports.initialize=(totalQuestions,reportView,slots)=>{const quiz=new _core.Quiz(Instructor);quiz.role.totalQuestions=totalQuestions,reportView?(Instructor.addReportEventHandlers(),quiz.role.responses.showResponses=!0,slots.forEach((slot=>{const wrapper="jazzquiz_wrapper_responses_"+slot.num,table="responses_wrapper_table_"+slot.num,graph="report_"+slot.num;quiz.role.responses.set(wrapper,table,slot.responses,void 0,slot.type,graph,!1)}))):quiz.poll(500)}}));
//# sourceMappingURL=instructor.min.js.map
\ No newline at end of file
diff --git a/amd/build/instructor.min.js.map b/amd/build/instructor.min.js.map
index 57c4289..6dec4aa 100644
--- a/amd/build/instructor.min.js.map
+++ b/amd/build/instructor.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"instructor.min.js","sources":["../src/instructor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Jazz from 'mod_jazzquiz/core';\nimport selectors from 'mod_jazzquiz/selectors';\n\nconst Quiz = Jazz.Quiz;\nconst Question = Jazz.Question;\nconst Ajax = Jazz.Ajax;\nconst setText = Jazz.setText;\n\nclass ResponseView {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.currentResponses = [];\n this.showVotesUponReview = false;\n this.respondedCount = 0;\n this.showResponses = false;\n this.totalStudents = 0;\n\n document.addEventListener('click', event => {\n const undoMergeButton = event.target.closest(selectors.quiz.undoMergeButton);\n if (undoMergeButton) {\n this.undoMerge();\n }\n\n const showNormalResultsButton = event.target.closest(selectors.quiz.showNormalResultsButton);\n if (showNormalResultsButton) {\n this.refresh(false);\n }\n\n const showVoteResultsButton = event.target.closest(selectors.quiz.showVoteResultsButton);\n if (showVoteResultsButton) {\n this.refreshVotes();\n }\n\n // Clicking a row to merge.\n if (event.target.classList.contains('bar')) {\n this.startMerge(event.target.id);\n } else if (event.target.parentNode && event.target.parentNode.classList.contains('bar')) {\n this.startMerge(event.target.parentNode.id);\n }\n });\n }\n\n /**\n * Clear, but not hide responses.\n */\n clear() {\n Quiz.responses.html('');\n Quiz.responseInfo.html('');\n }\n\n /**\n * Hides the responses\n */\n hide() {\n Quiz.uncheck(Instructor.control('responses'));\n Quiz.hide(Quiz.responses);\n Quiz.hide(Quiz.responseInfo);\n }\n\n /**\n * Shows the responses\n */\n show() {\n Quiz.check(Instructor.control('responses'));\n Quiz.show(Quiz.responses);\n Quiz.show(Quiz.responseInfo);\n if (this.showVotesUponReview) {\n this.refreshVotes();\n this.showVotesUponReview = false;\n } else {\n this.refresh(false);\n }\n }\n\n /**\n * Toggle whether to show or hide the responses\n */\n toggle() {\n this.showResponses = !this.showResponses;\n if (this.showResponses) {\n this.show();\n } else {\n this.hide();\n }\n }\n\n /**\n * End the response merge.\n */\n endMerge() {\n document.querySelectorAll(selectors.quiz.mergeInto).forEach(element => element.classList.remove('merge-into'));\n document.querySelectorAll(selectors.quiz.mergeFrom).forEach(element => element.classList.remove('merge-from'));\n }\n\n /**\n * Undo the last response merge.\n */\n undoMerge() {\n Ajax.post('undo_merge', {}, () => this.refresh(true));\n }\n\n /**\n * Merges responses based on response string.\n * @param {string} from\n * @param {string} into\n */\n merge(from, into) {\n Ajax.post('merge_responses', {from: from, into: into}, () => this.refresh(false));\n }\n\n /**\n * Start a merge between two responses.\n * @param {string} fromRowBarId\n */\n startMerge(fromRowBarId) {\n const barCell = document.getElementById(fromRowBarId);\n const row = barCell.parentElement;\n if (row.classList.contains('merge-from')) {\n this.endMerge();\n return;\n }\n if (row.classList.contains('merge-into')) {\n const fromRow = document.querySelector(selectors.quiz.mergeFrom);\n this.merge(fromRow.dataset.response, row.dataset.response);\n this.endMerge();\n return;\n }\n row.classList.add('merge-from');\n row.closest('table').querySelectorAll('tr').forEach(tableRow => {\n const cell = tableRow.querySelector('td:nth-child(2)');\n if (cell && cell.id !== barCell.id) {\n tableRow.classList.add('merge-into');\n }\n });\n }\n\n /**\n * Create controls to toggle between the responses of the actual question and the vote that followed.\n * @param {string} name Can be either 'vote_response' or 'current_response'\n */\n createControls(name) {\n if (!this.quiz.question.hasVotes) {\n Quiz.hide(Quiz.responseInfo);\n return;\n }\n // Add button for instructor to change what to review.\n if (this.quiz.state === 'reviewing') {\n let $showNormalResult = $('#review_show_normal_results');\n let $showVoteResult = $('#review_show_vote_results');\n Quiz.show(Quiz.responseInfo);\n if (name === 'vote_response') {\n if ($showNormalResult.length === 0) {\n setText(Quiz.responseInfo.html(' ').children('h4'), 'showing_vote_results');\n Quiz.responseInfo.append(' ');\n setText($('#review_show_normal_results'), 'click_to_show_original_results');\n $showVoteResult.remove();\n }\n } else if (name === 'current_response') {\n if ($showVoteResult.length === 0) {\n setText(Quiz.responseInfo.html(' ').children('h4'), 'showing_original_results');\n Quiz.responseInfo.append(' ');\n setText($('#review_show_vote_results'), 'click_to_show_vote_results');\n $showNormalResult.remove();\n }\n }\n }\n }\n\n addBarGraphRow(target, name, response, i, highestResponseCount) {\n // Const percent = (parseInt(responses[i].count) / total) * 100;\n const percent = (parseInt(response.count) / highestResponseCount) * 100;\n\n // Check if row with same response already exists.\n let rowIndex = -1;\n let currentRowIndex = -1;\n for (let j = 0; j < target.rows.length; j++) {\n if (target.rows[j].dataset.response === response.response) {\n rowIndex = parseInt(target.rows[j].dataset.rowIndex);\n currentRowIndex = j;\n break;\n }\n }\n\n if (rowIndex === -1) {\n rowIndex = target.rows.length;\n let row = target.insertRow();\n row.dataset.responseIndex = i;\n row.dataset.response = response.response;\n row.dataset.percent = percent;\n row.dataset.rowIndex = rowIndex;\n row.dataset.count = response.count;\n row.classList.add('selected-vote-option');\n if (percent < 15) {\n row.classList.add('outside');\n }\n\n const countHtml = '' + response.count + ' ';\n let responseCell = row.insertCell(0);\n responseCell.onclick = function() {\n $(this).parent().toggleClass('selected-vote-option');\n };\n\n let barCell = row.insertCell(1);\n barCell.classList.add('bar');\n barCell.id = name + '_bar_' + rowIndex;\n barCell.innerHTML = '' + countHtml + '
';\n\n const latexId = name + '_latex_' + rowIndex;\n responseCell.innerHTML = ' ';\n Quiz.addMathjaxElement($('#' + latexId), response.response);\n if (response.qtype === 'stack') {\n Quiz.renderMaximaEquation(response.response, latexId);\n }\n } else {\n let currentRow = target.rows[currentRowIndex];\n currentRow.dataset.rowIndex = rowIndex;\n currentRow.dataset.responseIndex = i;\n currentRow.dataset.percent = percent;\n currentRow.dataset.count = response.count;\n const containsOutside = currentRow.classList.contains('outside');\n if (percent > 15 && containsOutside) {\n currentRow.classList.remove('outside');\n } else if (percent < 15 && !containsOutside) {\n currentRow.classList.add('outside');\n }\n let countElement = document.getElementById(name + '_count_' + rowIndex);\n if (countElement !== null) {\n countElement.innerHTML = response.count;\n }\n let barElement = document.getElementById(name + '_bar_' + rowIndex);\n if (barElement !== null) {\n barElement.firstElementChild.style.width = percent + '%';\n }\n }\n }\n\n /**\n * Create a new and unsorted response bar graph.\n *\n * @param {Array.} responses\n * @param {string} name\n * @param {string} targetId\n * @param {string} graphId\n * @param {boolean} rebuild If the table should be completely rebuilt or not\n */\n createBarGraph(responses, name, targetId, graphId, rebuild) {\n let target = document.getElementById(targetId);\n if (target === null) {\n return;\n }\n let highestResponseCount = 0;\n for (let i = 0; i < responses.length; i++) {\n let count = parseInt(responses[i].count); // In case count is a string.\n if (count > highestResponseCount) {\n highestResponseCount = count;\n }\n }\n\n // Remove the rows if it should be rebuilt.\n if (rebuild) {\n target.innerHTML = '';\n }\n\n // Prune rows.\n for (let i = 0; i < target.rows.length; i++) {\n let prune = true;\n for (let j = 0; j < responses.length; j++) {\n if (target.rows[i].dataset.response === responses[j].response) {\n prune = false;\n break;\n }\n }\n if (prune) {\n target.deleteRow(i);\n i--;\n }\n }\n\n // Add rows.\n this.createControls(name);\n name += graphId;\n for (let i = 0; i < responses.length; i++) {\n this.addBarGraphRow(target, name, responses[i], i, highestResponseCount);\n }\n }\n\n /**\n * Sort the responses in the graph by how many had the same response.\n * @param {string} targetId\n */\n static sortBarGraph(targetId) {\n let target = document.getElementById(targetId);\n if (target === null) {\n return;\n }\n let isSorting = true;\n while (isSorting) {\n isSorting = false;\n for (let i = 0; i < (target.rows.length - 1); i++) {\n const current = parseInt(target.rows[i].dataset.percent);\n const next = parseInt(target.rows[i + 1].dataset.percent);\n if (current < next) {\n target.rows[i].parentNode.insertBefore(target.rows[i + 1], target.rows[i]);\n isSorting = true;\n break;\n }\n }\n }\n }\n\n /**\n * Create and sort a bar graph based on the responses passed.\n * @param {string} wrapperId\n * @param {string} tableId\n * @param {Array.} responses\n * @param {number|undefined} responded How many students responded to the question\n * @param {string} questionType\n * @param {string} graphId\n * @param {boolean} rebuild If the graph should be rebuilt or not.\n */\n set(wrapperId, tableId, responses, responded, questionType, graphId, rebuild) {\n if (responses === undefined) {\n return;\n }\n\n // Check if any responses to show.\n if (responses.length === 0) {\n Quiz.show(Quiz.responded);\n setText(Quiz.responded.find('h4'), 'a_out_of_b_responded', 'jazzquiz', {\n a: 0,\n b: this.totalStudents\n });\n return;\n }\n\n // Question type specific.\n switch (questionType) {\n case 'shortanswer':\n for (let i = 0; i < responses.length; i++) {\n responses[i].response = responses[i].response.trim();\n }\n break;\n case 'stack':\n // Remove all spaces from responses.\n for (let i = 0; i < responses.length; i++) {\n responses[i].response = responses[i].response.replace(/\\s/g, '');\n }\n break;\n default:\n break;\n }\n\n // Update data.\n this.currentResponses = [];\n this.respondedCount = 0;\n for (let i = 0; i < responses.length; i++) {\n let exists = false;\n let count = 1;\n if (responses[i].count !== undefined) {\n count = parseInt(responses[i].count);\n }\n this.respondedCount += count;\n // Check if response is a duplicate.\n for (let j = 0; j < this.currentResponses.length; j++) {\n if (this.currentResponses[j].response === responses[i].response) {\n this.currentResponses[j].count += count;\n exists = true;\n break;\n }\n }\n // Add element if not a duplicate.\n if (!exists) {\n this.currentResponses.push({\n response: responses[i].response,\n count: count,\n qtype: questionType\n });\n }\n }\n\n // Update responded container.\n if (Quiz.responded.length !== 0 && responded !== undefined) {\n Quiz.show(Quiz.responded);\n setText(Quiz.responded.find('h4'), 'a_out_of_b_responded', 'jazzquiz', {\n a: responded,\n b: this.totalStudents\n });\n }\n\n // Should we show the responses?\n if (!this.showResponses && this.quiz.state !== 'reviewing') {\n Quiz.hide(Quiz.responseInfo);\n Quiz.hide(Quiz.responses);\n return;\n }\n\n if (document.getElementById(tableId) === null) {\n const html = '';\n Quiz.show($('#' + wrapperId).html(html));\n }\n this.createBarGraph(this.currentResponses, 'current_response', tableId, graphId, rebuild);\n ResponseView.sortBarGraph(tableId);\n }\n\n /**\n * Fetch and show results for the ongoing or previous question.\n * @param {boolean} rebuild If the response graph should be rebuilt or not.\n */\n refresh(rebuild) {\n Ajax.get('get_results', {}, data => {\n this.quiz.question.hasVotes = data.has_votes;\n this.totalStudents = parseInt(data.total_students);\n\n this.set('jazzquiz_responses_container', 'current_responses_wrapper',\n data.responses, data.responded, data.question_type, 'results', rebuild);\n\n if (data.merge_count > 0) {\n Quiz.show($('#jazzquiz_undo_merge'));\n } else {\n Quiz.hide($('#jazzquiz_undo_merge'));\n }\n });\n }\n\n /**\n * Method refresh() equivalent for votes.\n */\n refreshVotes() {\n // Should we show the results?\n if (!this.showResponses && this.quiz.state !== 'reviewing') {\n Quiz.hide(Quiz.responseInfo);\n Quiz.hide(Quiz.responses);\n return;\n }\n Ajax.get('get_vote_results', {}, data => {\n const answers = data.answers;\n const targetId = 'wrapper_vote_responses';\n let responses = [];\n\n this.respondedCount = 0;\n this.totalStudents = parseInt(data.total_students);\n\n for (let i in answers) {\n if (!answers.hasOwnProperty(i)) {\n continue;\n }\n responses.push({\n response: answers[i].attempt,\n count: answers[i].finalcount,\n qtype: answers[i].qtype,\n slot: answers[i].slot\n });\n this.respondedCount += parseInt(answers[i].finalcount);\n }\n\n setText(Quiz.responded.find('h4'), 'a_out_of_b_voted', 'jazzquiz', {\n a: this.respondedCount,\n b: this.totalStudents\n });\n\n if (document.getElementById(targetId) === null) {\n const html = '';\n Quiz.show(Quiz.responses.html(html));\n }\n\n this.createBarGraph(responses, 'vote_response', targetId, 'vote', false);\n ResponseView.sortBarGraph(targetId);\n });\n }\n\n}\n\nclass Instructor {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.responses = new ResponseView(quiz);\n this.isShowingCorrectAnswer = false;\n this.totalQuestions = 0;\n this.allowVote = false;\n\n $(document).on('keyup', event => {\n if (event.keyCode === 27) { // Escape key.\n Instructor.closeFullscreenView();\n }\n });\n\n $(document).on('click', event => {\n Instructor.closeQuestionListMenu(event, 'improvise');\n Instructor.closeQuestionListMenu(event, 'jump');\n });\n\n Instructor.addEvents({\n 'repoll': () => this.repollQuestion(),\n 'vote': () => this.runVoting(),\n 'improvise': () => this.showQuestionListSetup('improvise'),\n 'jump': () => this.showQuestionListSetup('jump'),\n 'next': () => this.nextQuestion(),\n 'random': () => this.randomQuestion(),\n 'end': () => this.endQuestion(),\n 'fullscreen': () => Instructor.showFullscreenView(),\n 'answer': () => this.showCorrectAnswer(),\n 'responses': () => this.responses.toggle(),\n 'exit': () => this.closeSession(),\n 'quit': () => this.closeSession(),\n 'startquiz': () => this.startQuiz()\n });\n\n Instructor.addHotkeys({\n 't': 'responses',\n 'r': 'repoll',\n 'a': 'answer',\n 'e': 'end',\n 'j': 'jump',\n 'i': 'improvise',\n 'v': 'vote',\n 'n': 'next',\n 'm': 'random',\n 'f': 'fullscreen'\n });\n }\n\n static addHotkeys(keys) {\n for (let key in keys) {\n if (keys.hasOwnProperty(key)) {\n keys[key] = {\n action: keys[key],\n repeat: false // TODO: Maybe event.repeat becomes more standard?\n };\n $(document).on('keydown', event => {\n if (keys[key].repeat || event.ctrlKey) {\n return;\n }\n if (String.fromCharCode(event.which).toLowerCase() !== key) {\n return;\n }\n let focusedTag = $(':focus').prop('tagName');\n if (focusedTag !== undefined) {\n focusedTag = focusedTag.toLowerCase();\n if (focusedTag === 'input' || focusedTag === 'textarea') {\n return;\n }\n }\n event.preventDefault();\n keys[key].repeat = true;\n let $control = Instructor.control(keys[key].action);\n if ($control.length && !$control.prop('disabled')) {\n $control.click();\n }\n });\n $(document).on('keyup', event => {\n if (String.fromCharCode(event.which).toLowerCase() === key) {\n keys[key].repeat = false;\n }\n });\n }\n }\n }\n\n static addEvents(events) {\n for (let key in events) {\n if (events.hasOwnProperty(key)) {\n $(document).on('click', '#jazzquiz_control_' + key, () => {\n Instructor.enableControls([]);\n events[key]();\n });\n }\n }\n }\n\n static get controls() {\n return $('#jazzquiz_controls_box');\n }\n\n static get controlButtons() {\n return Instructor.controls.find('.quiz-control-buttons');\n }\n\n static control(key) {\n return $('#jazzquiz_control_' + key);\n }\n\n static get side() {\n return $('#jazzquiz_side_container');\n }\n\n static get correctAnswer() {\n return $('#jazzquiz_correct_answer_container');\n }\n\n static get isMerging() {\n return $('.merge-from').length !== 0;\n }\n\n onNotRunning(data) {\n this.responses.totalStudents = data.student_count;\n Quiz.hide(Instructor.side);\n setText(Quiz.info, 'instructions_for_instructor');\n Instructor.enableControls([]);\n Quiz.hide(Instructor.controlButtons);\n let $studentsJoined = Instructor.control('startquiz').next();\n if (data.student_count === 1) {\n setText($studentsJoined, 'one_student_has_joined');\n } else if (data.student_count > 1) {\n setText($studentsJoined, 'x_students_have_joined', 'jazzquiz', data.student_count);\n } else {\n setText($studentsJoined, 'no_students_have_joined');\n }\n Quiz.show(Instructor.control('startquiz').parent());\n }\n\n onPreparing(data) {\n Quiz.hide(Instructor.side);\n setText(Quiz.info, 'instructions_for_instructor');\n let enabledButtons = ['improvise', 'jump', 'random', 'fullscreen', 'quit'];\n if (data.slot < this.totalQuestions) {\n enabledButtons.push('next');\n }\n Instructor.enableControls(enabledButtons);\n }\n\n onRunning(data) {\n if (!this.responses.showResponses) {\n this.responses.hide();\n }\n Quiz.show(Instructor.side);\n Instructor.enableControls(['end', 'responses', 'fullscreen']);\n this.quiz.question.questionTime = data.questiontime;\n if (this.quiz.question.isRunning) {\n // Check if the question has already ended.\n // We need to do this because the state does not update unless an instructor is connected.\n if (data.questionTime > 0 && data.delay < -data.questiontime) {\n this.endQuestion();\n }\n // Only rebuild results if we are not merging.\n this.responses.refresh(!Instructor.isMerging);\n } else {\n const started = this.quiz.question.startCountdown(data.questiontime, data.delay);\n if (started) {\n this.quiz.question.isRunning = true;\n }\n }\n }\n\n onReviewing(data) {\n Quiz.show(Instructor.side);\n let enabledButtons = ['answer', 'repoll', 'fullscreen', 'improvise', 'jump', 'random', 'quit'];\n if (this.allowVote) {\n enabledButtons.push('vote');\n }\n if (data.slot < this.totalQuestions) {\n enabledButtons.push('next');\n }\n Instructor.enableControls(enabledButtons);\n\n // In case page was refreshed, we should ensure the question is showing.\n if (!Question.isLoaded()) {\n this.quiz.question.refresh();\n }\n\n // For now, just always show responses while reviewing.\n // In the future, there should be an additional toggle.\n if (this.quiz.isNewState) {\n this.responses.show();\n }\n // No longer in question.\n this.quiz.question.isRunning = false;\n }\n\n onSessionClosed() {\n Quiz.hide(Instructor.side);\n Quiz.hide(Instructor.correctAnswer);\n Instructor.enableControls([]);\n this.responses.clear();\n this.quiz.question.isRunning = false;\n }\n\n onVoting() {\n if (!this.responses.showResponses) {\n this.responses.hide();\n }\n Quiz.show(Instructor.side);\n Instructor.enableControls(['quit', 'fullscreen', 'answer', 'responses', 'end']);\n this.responses.refreshVotes();\n }\n\n onStateChange() {\n $('#region-main').find('ul.nav.nav-tabs').css('display', 'none');\n $('#region-main-settings-menu').css('display', 'none');\n $('.region_main_settings_menu_proxy').css('display', 'none');\n Quiz.show(Instructor.controlButtons);\n Quiz.hide(Instructor.control('startquiz').parent());\n }\n\n onQuestionRefreshed(data) {\n this.allowVote = data.voteable;\n }\n\n onTimerEnding() {\n this.endQuestion();\n }\n\n onTimerTick(timeLeft) {\n setText(Question.timer, 'x_seconds_left', 'jazzquiz', timeLeft);\n }\n\n /**\n * Start the quiz. Does not start any questions.\n */\n startQuiz() {\n Quiz.hide(Instructor.control('startquiz').parent());\n Ajax.post('start_quiz', {}, () => $('#jazzquiz_controls').removeClass('btn-hide'));\n }\n\n /**\n * End the currently ongoing question or vote.\n */\n endQuestion() {\n this.quiz.question.hideTimer();\n Ajax.post('end_question', {}, () => {\n if (this.quiz.state === 'voting') {\n this.responses.showVotesUponReview = true;\n } else {\n this.quiz.question.isRunning = false;\n Instructor.enableControls([]);\n }\n });\n }\n\n /**\n * Show a question list dropdown.\n * @param {string} name\n */\n showQuestionListSetup(name) {\n let $controlButton = Instructor.control(name);\n if ($controlButton.hasClass('active')) {\n // It's already open. Let's not send another request.\n return;\n }\n Ajax.get('list_' + name + '_questions', {}, data => {\n let self = this;\n let $menu = $('#jazzquiz_' + name + '_menu');\n const menuMargin = $controlButton.offset().left - $controlButton.parent().offset().left;\n $menu.html('').addClass('active').css('margin-left', menuMargin + 'px');\n $controlButton.addClass('active');\n const questions = data.questions;\n for (let i in questions) {\n if (!questions.hasOwnProperty(i)) {\n continue;\n }\n let $questionButton = $(' ');\n Quiz.addMathjaxElement($questionButton, questions[i].name);\n $questionButton.data({\n 'time': questions[i].time,\n 'question-id': questions[i].questionid,\n 'jazzquiz-question-id': questions[i].jazzquizquestionid\n });\n $questionButton.data('test', 1);\n $questionButton.on('click', function() {\n const questionId = $(this).data('question-id');\n const time = $(this).data('time');\n const jazzQuestionId = $(this).data('jazzquiz-question-id');\n self.jumpQuestion(questionId, time, jazzQuestionId);\n $menu.html('').removeClass('active');\n $controlButton.removeClass('active');\n });\n $menu.append($questionButton);\n }\n });\n }\n\n /**\n * Get the selected responses.\n * @returns {Array.} Vote options\n */\n static getSelectedAnswersForVote() {\n let result = [];\n $('.selected-vote-option').each((i, option) => {\n result.push({\n text: option.dataset.response,\n count: option.dataset.count\n });\n });\n return result;\n }\n\n /**\n * Start a vote with the responses that are currently selected.\n */\n runVoting() {\n const options = Instructor.getSelectedAnswersForVote();\n const data = {questions: encodeURIComponent(JSON.stringify(options))};\n Ajax.post('run_voting', data);\n }\n\n /**\n * Start a new question in this session.\n * @param {string} method\n * @param {number} questionId\n * @param {number} questionTime\n * @param {number} jazzquizQuestionId\n */\n startQuestion(method, questionId, questionTime, jazzquizQuestionId) {\n Quiz.hide(Quiz.info);\n this.responses.clear();\n this.hideCorrectAnswer();\n Ajax.post('start_question', {\n method: method,\n questionid: questionId,\n questiontime: questionTime,\n jazzquizquestionid: jazzquizQuestionId\n }, data => this.quiz.question.startCountdown(data.questiontime, data.delay));\n }\n\n /**\n * Jump to a planned question in the quiz.\n * @param {number} questionId\n * @param {number} questionTime\n * @param {number} jazzquizQuestionId\n */\n jumpQuestion(questionId, questionTime, jazzquizQuestionId) {\n this.startQuestion('jump', questionId, questionTime, jazzquizQuestionId);\n }\n\n /**\n * Repoll the previously asked question.\n */\n repollQuestion() {\n this.startQuestion('repoll', 0, 0, 0);\n }\n\n /**\n * Continue on to the next preplanned question.\n */\n nextQuestion() {\n this.startQuestion('next', 0, 0, 0);\n }\n\n /**\n * Start a random question.\n */\n randomQuestion() {\n this.startQuestion('random', 0, 0, 0);\n }\n\n /**\n * Close the current session.\n */\n closeSession() {\n Quiz.hide($('#jazzquiz_undo_merge'));\n Quiz.hide(Question.box);\n Quiz.hide(Instructor.controls);\n setText(Quiz.info, 'closing_session');\n // eslint-disable-next-line no-return-assign\n Ajax.post('close_session', {}, () => window.location = location.href.split('&')[0]);\n }\n\n /**\n * Hide the correct answer if showing.\n */\n hideCorrectAnswer() {\n if (this.isShowingCorrectAnswer) {\n Quiz.hide(Instructor.correctAnswer);\n Quiz.uncheck(Instructor.control('answer'));\n this.isShowingCorrectAnswer = false;\n }\n }\n\n /**\n * Request and show the correct answer for the ongoing or previous question.\n */\n showCorrectAnswer() {\n this.hideCorrectAnswer();\n Ajax.get('get_right_response', {}, data => {\n Quiz.show(Instructor.correctAnswer.html(data.right_answer));\n Quiz.renderAllMathjax();\n Quiz.check(Instructor.control('answer'));\n this.isShowingCorrectAnswer = true;\n });\n }\n\n /**\n * Enables all buttons passed in arguments, but disables all others.\n * @param {Array.} buttons The unique part of the IDs of the buttons to be enabled.\n */\n static enableControls(buttons) {\n Instructor.controlButtons.children('button').each((index, child) => {\n const id = child.getAttribute('id').replace('jazzquiz_control_', '');\n child.disabled = (buttons.indexOf(id) === -1);\n });\n }\n\n /**\n * Enter fullscreen mode for better use with projectors.\n */\n static showFullscreenView() {\n if (Quiz.main.hasClass('jazzquiz-fullscreen')) {\n Instructor.closeFullscreenView();\n return;\n }\n // Hide the scrollbar - remember to always set back to auto when closing.\n document.documentElement.style.overflowY = 'hidden';\n // Sets the quiz view to an absolute position that covers the viewport.\n Quiz.main.addClass('jazzquiz-fullscreen');\n }\n\n /**\n * Exit the fullscreen mode.\n */\n static closeFullscreenView() {\n document.documentElement.style.overflowY = 'auto';\n Quiz.main.removeClass('jazzquiz-fullscreen');\n }\n\n /**\n * Close the dropdown menu for choosing a question.\n * @param {Event} event\n * @param {string} name\n */\n static closeQuestionListMenu(event, name) {\n const menuId = '#jazzquiz_' + name + '_menu';\n // Close the menu if the click was not inside.\n const menu = $(event.target).closest(menuId);\n if (!menu.length) {\n $(menuId).html('').removeClass('active');\n Instructor.control(name).removeClass('active');\n }\n }\n\n static addReportEventHandlers() {\n $(document).on('click', '#report_overview_controls button', function() {\n const action = $(this).data('action');\n if (action === 'attendance') {\n $('#report_overview_responded').fadeIn();\n $('#report_overview_responses').fadeOut();\n } else if (action === 'responses') {\n $('#report_overview_responses').fadeIn();\n $('#report_overview_responded').fadeOut();\n }\n });\n }\n\n}\n\nexport const initialize = (totalQuestions, reportView, slots) => {\n let quiz = new Quiz(Instructor);\n quiz.role.totalQuestions = totalQuestions;\n if (reportView) {\n Instructor.addReportEventHandlers();\n quiz.role.responses.showResponses = true;\n slots.forEach(slot => {\n const wrapper = 'jazzquiz_wrapper_responses_' + slot.num;\n const table = 'responses_wrapper_table_' + slot.num;\n const graph = 'report_' + slot.num;\n quiz.role.responses.set(wrapper, table, slot.responses, undefined, slot.type, graph, false);\n });\n } else {\n quiz.poll(500);\n }\n};\n"],"names":["Quiz","Jazz","Question","Ajax","setText","ResponseView","constructor","quiz","currentResponses","showVotesUponReview","respondedCount","showResponses","totalStudents","document","addEventListener","event","target","closest","selectors","undoMergeButton","undoMerge","showNormalResultsButton","refresh","showVoteResultsButton","refreshVotes","classList","contains","startMerge","id","parentNode","clear","responses","html","responseInfo","hide","uncheck","Instructor","control","show","check","this","toggle","endMerge","querySelectorAll","mergeInto","forEach","element","remove","mergeFrom","post","merge","from","into","fromRowBarId","barCell","getElementById","row","parentElement","fromRow","querySelector","dataset","response","add","tableRow","cell","createControls","name","question","hasVotes","state","$showNormalResult","$showVoteResult","length","children","append","addBarGraphRow","i","highestResponseCount","percent","parseInt","count","rowIndex","currentRowIndex","j","rows","insertRow","responseIndex","countHtml","responseCell","insertCell","onclick","parent","toggleClass","innerHTML","latexId","addMathjaxElement","qtype","renderMaximaEquation","currentRow","containsOutside","countElement","barElement","firstElementChild","style","width","createBarGraph","targetId","graphId","rebuild","prune","deleteRow","isSorting","insertBefore","set","wrapperId","tableId","responded","questionType","undefined","find","a","b","trim","replace","exists","push","sortBarGraph","get","data","has_votes","total_students","question_type","merge_count","answers","hasOwnProperty","attempt","finalcount","slot","isShowingCorrectAnswer","totalQuestions","allowVote","on","keyCode","closeFullscreenView","closeQuestionListMenu","addEvents","repollQuestion","runVoting","showQuestionListSetup","nextQuestion","randomQuestion","endQuestion","showFullscreenView","showCorrectAnswer","closeSession","startQuiz","addHotkeys","keys","key","action","repeat","ctrlKey","String","fromCharCode","which","toLowerCase","focusedTag","prop","preventDefault","$control","click","events","enableControls","controls","controlButtons","side","correctAnswer","isMerging","onNotRunning","student_count","info","$studentsJoined","next","onPreparing","enabledButtons","onRunning","questionTime","questiontime","isRunning","delay","startCountdown","onReviewing","isLoaded","isNewState","onSessionClosed","onVoting","onStateChange","css","onQuestionRefreshed","voteable","onTimerEnding","onTimerTick","timeLeft","timer","removeClass","hideTimer","$controlButton","hasClass","self","$menu","menuMargin","offset","left","addClass","questions","$questionButton","time","questionid","jazzquizquestionid","questionId","jazzQuestionId","jumpQuestion","result","each","option","text","options","getSelectedAnswersForVote","encodeURIComponent","JSON","stringify","startQuestion","method","jazzquizQuestionId","hideCorrectAnswer","box","window","location","href","split","right_answer","renderAllMathjax","buttons","index","child","getAttribute","disabled","indexOf","main","documentElement","overflowY","menuId","fadeIn","fadeOut","reportView","slots","role","addReportEventHandlers","wrapper","num","table","graph","type","poll"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"instructor.min.js","sources":["../src/instructor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {Quiz, Ajax, setText} from 'mod_jazzquiz/core';\nimport {Question} from 'mod_jazzquiz/question';\nimport selectors from 'mod_jazzquiz/selectors';\n\nclass ResponseView {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.currentResponses = [];\n this.showVotesUponReview = false;\n this.respondedCount = 0;\n this.showResponses = false;\n this.totalStudents = 0;\n\n document.addEventListener('click', event => {\n const undoMergeButton = event.target.closest(selectors.quiz.undoMergeButton);\n if (undoMergeButton) {\n this.undoMerge();\n }\n\n const showNormalResultsButton = event.target.closest(selectors.quiz.showNormalResultsButton);\n if (showNormalResultsButton) {\n this.refresh(false);\n }\n\n const showVoteResultsButton = event.target.closest(selectors.quiz.showVoteResultsButton);\n if (showVoteResultsButton) {\n this.refreshVotes();\n }\n\n // Clicking a row to merge.\n if (event.target.classList.contains('bar')) {\n this.startMerge(event.target.id);\n } else if (event.target.parentNode && event.target.parentNode.classList.contains('bar')) {\n this.startMerge(event.target.parentNode.id);\n }\n });\n }\n\n /**\n * Clear, but not hide responses.\n */\n clear() {\n document.querySelector(selectors.quiz.responses).innerHTML = '';\n document.querySelector(selectors.quiz.responseInfo).innerHTML = '';\n }\n\n /**\n * Hides the responses\n */\n hide() {\n Instructor.control('responses').querySelector('.fa').classList.replace('fa-check-square-o', 'fa-square-o');\n document.querySelector(selectors.quiz.responses).classList.add('hidden');\n document.querySelector(selectors.quiz.responseInfo).classList.add('hidden');\n }\n\n /**\n * Shows the responses\n */\n show() {\n Instructor.control('responses').querySelector('.fa').classList.replace('fa-square-o', 'fa-check-square-o');\n document.querySelector(selectors.quiz.responses).classList.remove('hidden');\n document.querySelector(selectors.quiz.responseInfo).classList.remove('hidden');\n if (this.showVotesUponReview) {\n this.refreshVotes();\n this.showVotesUponReview = false;\n } else {\n this.refresh(false);\n }\n }\n\n /**\n * Toggle whether to show or hide the responses\n */\n toggle() {\n this.showResponses = !this.showResponses;\n if (this.showResponses) {\n this.show();\n } else {\n this.hide();\n }\n }\n\n /**\n * End the response merge.\n */\n endMerge() {\n document.querySelectorAll(selectors.quiz.mergeInto).forEach(element => element.classList.remove('merge-into'));\n document.querySelectorAll(selectors.quiz.mergeFrom).forEach(element => element.classList.remove('merge-from'));\n }\n\n /**\n * Undo the last response merge.\n */\n undoMerge() {\n Ajax.post('undo_merge', {}, () => this.refresh(true));\n }\n\n /**\n * Merges responses based on response string.\n * @param {string} from\n * @param {string} into\n */\n merge(from, into) {\n Ajax.post('merge_responses', {from: from, into: into}, () => this.refresh(false));\n }\n\n /**\n * Start a merge between two responses.\n * @param {string} fromRowBarId\n */\n startMerge(fromRowBarId) {\n const barCell = document.getElementById(fromRowBarId);\n const row = barCell.parentElement;\n if (row.classList.contains('merge-from')) {\n this.endMerge();\n return;\n }\n if (row.classList.contains('merge-into')) {\n const fromRow = document.querySelector(selectors.quiz.mergeFrom);\n this.merge(fromRow.dataset.response, row.dataset.response);\n this.endMerge();\n return;\n }\n row.classList.add('merge-from');\n row.closest('table').querySelectorAll('tr').forEach(tableRow => {\n const cell = tableRow.querySelector('td:nth-child(2)');\n if (cell && cell.id !== barCell.id) {\n tableRow.classList.add('merge-into');\n }\n });\n }\n\n /**\n * Create controls to toggle between the responses of the actual question and the vote that followed.\n * @param {string} name Can be either 'vote_response' or 'current_response'\n */\n createControls(name) {\n const quizResponseInfo = document.querySelector(selectors.quiz.responseInfo);\n if (!this.quiz.question.hasVotes) {\n quizResponseInfo.classList.add('hidden');\n return;\n }\n // Add button for instructor to change what to review.\n if (this.quiz.state === 'reviewing') {\n let showNormalResult = document.querySelector(selectors.quiz.showNormalResultsButton);\n let showVoteResult = document.querySelector(selectors.quiz.showVoteResultsButton);\n quizResponseInfo.classList.remove('hidden');\n if (name === 'vote_response') {\n if (!showNormalResult) {\n setText(quizResponseInfo.html(' ').children('h4'), 'showing_vote_results');\n quizResponseInfo.innerHTML += ' ';\n showNormalResult = document.querySelector(selectors.quiz.showNormalResultsButton);\n setText(showNormalResult, 'click_to_show_original_results');\n showVoteResult.remove();\n }\n } else if (name === 'current_response') {\n if (!showVoteResult) {\n quizResponseInfo.innerHTML = ' ';\n setText(quizResponseInfo.querySelector('h4'), 'showing_original_results');\n quizResponseInfo.innerHTML += ' ';\n showVoteResult = document.querySelector(selectors.quiz.showVoteResultsButton);\n setText(showVoteResult, 'click_to_show_vote_results');\n showNormalResult.remove();\n }\n }\n }\n }\n\n addBarGraphRow(target, name, response, i, highestResponseCount) {\n // Const percent = (parseInt(responses[i].count) / total) * 100;\n const percent = (parseInt(response.count) / highestResponseCount) * 100;\n\n // Check if row with same response already exists.\n let rowIndex = -1;\n let currentRowIndex = -1;\n for (let j = 0; j < target.rows.length; j++) {\n if (target.rows[j].dataset.response === response.response) {\n rowIndex = parseInt(target.rows[j].dataset.rowIndex);\n currentRowIndex = j;\n break;\n }\n }\n\n if (rowIndex === -1) {\n rowIndex = target.rows.length;\n let row = target.insertRow();\n row.dataset.responseIndex = i;\n row.dataset.response = response.response;\n row.dataset.percent = percent;\n row.dataset.rowIndex = rowIndex;\n row.dataset.count = response.count;\n row.classList.add('selected-vote-option');\n if (percent < 15) {\n row.classList.add('outside');\n }\n\n const countHtml = '' + response.count + ' ';\n let responseCell = row.insertCell(0);\n responseCell.onclick = () => responseCell.parentElement.classList.toggle('selected-vote-option');\n\n let barCell = row.insertCell(1);\n barCell.classList.add('bar');\n barCell.id = name + '_bar_' + rowIndex;\n barCell.innerHTML = '' + countHtml + '
';\n\n const latexId = name + '_latex_' + rowIndex;\n responseCell.innerHTML = ' ';\n Quiz.addMathjaxElement(document.getElementById(latexId), response.response);\n if (response.qtype === 'stack') {\n Quiz.renderMaximaEquation(response.response, latexId);\n }\n } else {\n let currentRow = target.rows[currentRowIndex];\n currentRow.dataset.rowIndex = rowIndex;\n currentRow.dataset.responseIndex = i;\n currentRow.dataset.percent = percent;\n currentRow.dataset.count = response.count;\n const containsOutside = currentRow.classList.contains('outside');\n if (percent > 15 && containsOutside) {\n currentRow.classList.remove('outside');\n } else if (percent < 15 && !containsOutside) {\n currentRow.classList.add('outside');\n }\n let countElement = document.getElementById(name + '_count_' + rowIndex);\n if (countElement !== null) {\n countElement.innerHTML = response.count;\n }\n let barElement = document.getElementById(name + '_bar_' + rowIndex);\n if (barElement !== null) {\n barElement.firstElementChild.style.width = percent + '%';\n }\n }\n }\n\n /**\n * Create a new and unsorted response bar graph.\n *\n * @param {Array.} responses\n * @param {string} name\n * @param {string} targetId\n * @param {string} graphId\n * @param {boolean} rebuild If the table should be completely rebuilt or not\n */\n createBarGraph(responses, name, targetId, graphId, rebuild) {\n let target = document.getElementById(targetId);\n if (target === null) {\n return;\n }\n let highestResponseCount = 0;\n for (let i = 0; i < responses.length; i++) {\n let count = parseInt(responses[i].count); // In case count is a string.\n if (count > highestResponseCount) {\n highestResponseCount = count;\n }\n }\n\n // Remove the rows if it should be rebuilt.\n if (rebuild) {\n target.innerHTML = '';\n }\n\n // Prune rows.\n for (let i = 0; i < target.rows.length; i++) {\n let prune = true;\n for (let j = 0; j < responses.length; j++) {\n if (target.rows[i].dataset.response === responses[j].response) {\n prune = false;\n break;\n }\n }\n if (prune) {\n target.deleteRow(i);\n i--;\n }\n }\n\n // Add rows.\n this.createControls(name);\n name += graphId;\n for (let i = 0; i < responses.length; i++) {\n this.addBarGraphRow(target, name, responses[i], i, highestResponseCount);\n }\n }\n\n /**\n * Sort the responses in the graph by how many had the same response.\n * @param {string} targetId\n */\n static sortBarGraph(targetId) {\n let target = document.getElementById(targetId);\n if (target === null) {\n return;\n }\n let isSorting = true;\n while (isSorting) {\n isSorting = false;\n for (let i = 0; i < (target.rows.length - 1); i++) {\n const current = parseInt(target.rows[i].dataset.percent);\n const next = parseInt(target.rows[i + 1].dataset.percent);\n if (current < next) {\n target.rows[i].parentNode.insertBefore(target.rows[i + 1], target.rows[i]);\n isSorting = true;\n break;\n }\n }\n }\n }\n\n /**\n * Create and sort a bar graph based on the responses passed.\n * @param {string} wrapperId\n * @param {string} tableId\n * @param {Array.} responses\n * @param {number|undefined} responded How many students responded to the question\n * @param {string} questionType\n * @param {string} graphId\n * @param {boolean} rebuild If the graph should be rebuilt or not.\n */\n set(wrapperId, tableId, responses, responded, questionType, graphId, rebuild) {\n if (responses === undefined) {\n return;\n }\n const quizResponded = document.querySelector(selectors.quiz.responded);\n\n // Check if any responses to show.\n if (responses.length === 0) {\n quizResponded.classList.remove('hidden');\n setText(quizResponded.querySelector('h4'), 'a_out_of_b_responded', 'jazzquiz', {\n a: 0,\n b: this.totalStudents\n });\n return;\n }\n\n // Question type specific.\n switch (questionType) {\n case 'shortanswer':\n for (let i = 0; i < responses.length; i++) {\n responses[i].response = responses[i].response.trim();\n }\n break;\n case 'stack':\n // Remove all spaces from responses.\n for (let i = 0; i < responses.length; i++) {\n responses[i].response = responses[i].response.replace(/\\s/g, '');\n }\n break;\n default:\n break;\n }\n\n // Update data.\n this.currentResponses = [];\n this.respondedCount = 0;\n for (let i = 0; i < responses.length; i++) {\n let exists = false;\n let count = 1;\n if (responses[i].count !== undefined) {\n count = parseInt(responses[i].count);\n }\n this.respondedCount += count;\n // Check if response is a duplicate.\n for (let j = 0; j < this.currentResponses.length; j++) {\n if (this.currentResponses[j].response === responses[i].response) {\n this.currentResponses[j].count += count;\n exists = true;\n break;\n }\n }\n // Add element if not a duplicate.\n if (!exists) {\n this.currentResponses.push({\n response: responses[i].response,\n count: count,\n qtype: questionType\n });\n }\n }\n\n // Update responded container.\n if (quizResponded.length !== 0 && responded !== undefined) {\n quizResponded.classList.remove('hidden');\n setText(quizResponded.querySelector('h4'), 'a_out_of_b_responded', 'jazzquiz', {\n a: responded,\n b: this.totalStudents\n });\n }\n\n // Should we show the responses?\n if (!this.showResponses && this.quiz.state !== 'reviewing') {\n document.querySelector(selectors.quiz.responseInfo).classList.add('hidden');\n document.querySelector(selectors.quiz.responses).classList.add('hidden');\n return;\n }\n\n if (document.getElementById(tableId) === null) {\n const wrapper = document.getElementById(wrapperId);\n wrapper.innerHTML = ``;\n wrapper.classList.remove('hidden');\n }\n this.createBarGraph(this.currentResponses, 'current_response', tableId, graphId, rebuild);\n ResponseView.sortBarGraph(tableId);\n }\n\n /**\n * Fetch and show results for the ongoing or previous question.\n * @param {boolean} rebuild If the response graph should be rebuilt or not.\n */\n refresh(rebuild) {\n Ajax.get('get_results', {}, data => {\n this.quiz.question.hasVotes = data.has_votes;\n this.totalStudents = parseInt(data.total_students);\n\n this.set('jazzquiz_responses_container', 'current_responses_wrapper',\n data.responses, data.responded, data.question_type, 'results', rebuild);\n\n const undoMergeButton = document.querySelector(selectors.quiz.undoMergeButton);\n if (undoMergeButton) {\n undoMergeButton.classList.toggle('hidden', data.merge_count <= 0);\n }\n });\n }\n\n /**\n * Method refresh() equivalent for votes.\n */\n refreshVotes() {\n const quizResponses = document.querySelector(selectors.quiz.responses);\n const quizResponseInfo = document.querySelector(selectors.quiz.responseInfo);\n // Should we show the results?\n if (!this.showResponses && this.quiz.state !== 'reviewing') {\n quizResponseInfo.classList.add('hidden');\n quizResponses.classList.add('hidden');\n return;\n }\n Ajax.get('get_vote_results', {}, data => {\n const answers = data.answers;\n const targetId = 'wrapper_vote_responses';\n let responses = [];\n this.respondedCount = 0;\n this.totalStudents = parseInt(data.total_students);\n for (let i in answers) {\n if (!answers.hasOwnProperty(i)) {\n continue;\n }\n responses.push({\n response: answers[i].attempt,\n count: answers[i].finalcount,\n qtype: answers[i].qtype,\n slot: answers[i].slot\n });\n this.respondedCount += parseInt(answers[i].finalcount);\n }\n setText(document.querySelector(selectors.quiz.responded + ' h4'), 'a_out_of_b_voted', 'jazzquiz', {\n a: this.respondedCount,\n b: this.totalStudents\n });\n if (document.getElementById(targetId) === null) {\n quizResponses.innerHTML = ``;\n quizResponses.classList.remove('hidden');\n }\n this.createBarGraph(responses, 'vote_response', targetId, 'vote', false);\n ResponseView.sortBarGraph(targetId);\n });\n }\n\n}\n\nclass Instructor {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.responses = new ResponseView(quiz);\n this.isShowingCorrectAnswer = false;\n this.totalQuestions = 0;\n this.allowVote = false;\n\n document.addEventListener('keyup', event => {\n if (event.key === 'Escape') {\n Instructor.closeFullscreenView();\n }\n });\n\n Instructor.addEvents({\n 'repoll': () => this.repollQuestion(),\n 'vote': () => this.runVoting(),\n 'improvise': () => this.showQuestionListSetup('improvise'),\n 'jump': () => this.showQuestionListSetup('jump'),\n 'next': () => this.nextQuestion(),\n 'random': () => this.randomQuestion(),\n 'end': () => this.endQuestion(),\n 'fullscreen': () => Instructor.showFullscreenView(),\n 'answer': () => this.showCorrectAnswer(),\n 'responses': () => this.responses.toggle(),\n 'exit': () => this.closeSession(),\n 'quit': () => this.closeSession(),\n 'startquiz': () => this.startQuiz()\n });\n\n Instructor.addHotkeys({\n 't': 'responses',\n 'r': 'repoll',\n 'a': 'answer',\n 'e': 'end',\n 'j': 'jump',\n 'i': 'improvise',\n 'v': 'vote',\n 'n': 'next',\n 'm': 'random',\n 'f': 'fullscreen'\n });\n\n document.addEventListener('click', event => {\n Instructor.closeQuestionListMenu(event, 'improvise');\n Instructor.closeQuestionListMenu(event, 'jump');\n if (event.target.closest(selectors.quiz.startQuizButton)) {\n this.startQuiz();\n }\n if (event.target.closest(selectors.quiz.exitQuizButton)) {\n this.closeSession();\n }\n });\n }\n\n static addHotkeys(keys) {\n for (let key in keys) {\n if (keys.hasOwnProperty(key)) {\n keys[key] = {\n action: keys[key],\n repeat: false // TODO: Maybe event.repeat becomes more standard?\n };\n\n document.addEventListener('keydown', event => {\n if (keys[key].repeat || event.ctrlKey) {\n return;\n }\n if (event.key.toLowerCase() !== key) {\n return;\n }\n let focusedTag = $(':focus').prop('tagName');\n if (focusedTag !== undefined) {\n focusedTag = focusedTag.toLowerCase();\n if (focusedTag === 'input' || focusedTag === 'textarea') {\n return;\n }\n }\n event.preventDefault();\n keys[key].repeat = true;\n let $control = Instructor.control(keys[key].action);\n if ($control.length && !$control.prop('disabled')) {\n $control.click();\n }\n });\n\n document.addEventListener('keyup', event => {\n if (event.key.toLowerCase() === key) {\n keys[key].repeat = false;\n }\n });\n }\n }\n }\n\n static addEvents(events) {\n document.addEventListener('click', event => {\n const controlButton = event.target.closest(selectors.quiz.controlButton);\n if (controlButton) {\n Instructor.enableControls([]);\n events[controlButton.dataset.control]();\n }\n });\n }\n\n static get controls() {\n return document.querySelector(selectors.quiz.controlsBox);\n }\n\n static get controlButtons() {\n return document.querySelector(selectors.quiz.controlButtons);\n }\n\n static control(key) {\n return document.getElementById(`jazzquiz_control_${key}`);\n }\n\n static get side() {\n return document.querySelector(selectors.quiz.sideContainer);\n }\n\n static get correctAnswer() {\n return document.querySelector(selectors.quiz.correctAnswerContainer);\n }\n\n static get isMerging() {\n return $('.merge-from').length !== 0;\n }\n\n onNotRunning(data) {\n this.responses.totalStudents = data.student_count;\n Instructor.side.classList.add('hidden');\n setText(document.querySelector(selectors.quiz.info), 'instructions_for_instructor');\n Instructor.enableControls([]);\n document.querySelector(selectors.quiz.controlButtons).classList.add('hidden');\n const studentsJoined = document.querySelector(selectors.quiz.studentsJoinedCounter);\n if (data.student_count === 1) {\n setText(studentsJoined, 'one_student_has_joined');\n } else if (data.student_count > 1) {\n setText(studentsJoined, 'x_students_have_joined', 'jazzquiz', data.student_count);\n } else {\n setText(studentsJoined, 'no_students_have_joined');\n }\n const startQuizButton = Instructor.control('startquiz');\n startQuizButton.parentElement.classList.remove('hidden');\n }\n\n onPreparing(data) {\n Instructor.side.classList.add('hidden');\n setText(document.querySelector(selectors.quiz.info), 'instructions_for_instructor');\n let enabledButtons = ['improvise', 'jump', 'random', 'fullscreen', 'quit'];\n if (data.slot < this.totalQuestions) {\n enabledButtons.push('next');\n }\n Instructor.enableControls(enabledButtons);\n }\n\n onRunning(data) {\n if (!this.responses.showResponses) {\n this.responses.hide();\n }\n Instructor.side.classList.remove('hidden');\n Instructor.enableControls(['end', 'responses', 'fullscreen']);\n this.quiz.question.questionTime = data.questiontime;\n if (this.quiz.question.isRunning) {\n // Check if the question has already ended.\n // We need to do this because the state does not update unless an instructor is connected.\n if (data.questionTime > 0 && data.delay < -data.questiontime) {\n this.endQuestion();\n }\n // Only rebuild results if we are not merging.\n this.responses.refresh(!Instructor.isMerging);\n } else {\n const started = this.quiz.question.startCountdown(data.questiontime, data.delay);\n if (started) {\n this.quiz.question.isRunning = true;\n }\n }\n }\n\n onReviewing(data) {\n Instructor.side.classList.remove('hidden');\n let enabledButtons = ['answer', 'repoll', 'fullscreen', 'improvise', 'jump', 'random', 'quit'];\n if (this.allowVote) {\n enabledButtons.push('vote');\n }\n if (data.slot < this.totalQuestions) {\n enabledButtons.push('next');\n }\n Instructor.enableControls(enabledButtons);\n\n // In case page was refreshed, we should ensure the question is showing.\n if (!Question.isLoaded()) {\n this.quiz.question.refresh();\n }\n\n // For now, just always show responses while reviewing.\n // In the future, there should be an additional toggle.\n if (this.quiz.isNewState) {\n this.responses.show();\n }\n // No longer in question.\n this.quiz.question.isRunning = false;\n }\n\n onSessionClosed() {\n Instructor.side.classList.add('hidden');\n Instructor.correctAnswer.classList.add('hidden');\n Instructor.enableControls([]);\n this.responses.clear();\n this.quiz.question.isRunning = false;\n }\n\n onVoting() {\n if (!this.responses.showResponses) {\n this.responses.hide();\n }\n Instructor.side.classList.remove('hidden');\n Instructor.enableControls(['quit', 'fullscreen', 'answer', 'responses', 'end']);\n this.responses.refreshVotes();\n }\n\n onStateChange() {\n //$('#region-main').find('ul.nav.nav-tabs').css('display', 'none');\n //$('#region-main-settings-menu').css('display', 'none');\n //$('.region_main_settings_menu_proxy').css('display', 'none');\n\n Instructor.controlButtons.classList.remove('hidden');\n Instructor.control('startquiz').parentElement.classList.add('hidden');\n\n }\n\n onQuestionRefreshed(data) {\n this.allowVote = data.voteable;\n }\n\n onTimerEnding() {\n this.endQuestion();\n }\n\n onTimerTick(timeLeft) {\n setText(document.querySelector(selectors.question.timer), 'x_seconds_left', 'jazzquiz', timeLeft);\n }\n\n /**\n * Start the quiz. Does not start any questions.\n */\n startQuiz() {\n Instructor.control('startquiz').parentElement.classList.add('hidden');\n Ajax.post('start_quiz', {}, () => {\n const controls = document.querySelector(selectors.quiz.controls);\n if (controls) {\n controls.classList.remove('btn-hide');\n }\n });\n }\n\n /**\n * End the currently ongoing question or vote.\n */\n endQuestion() {\n this.quiz.question.hideTimer();\n Ajax.post('end_question', {}, () => {\n if (this.quiz.state === 'voting') {\n this.responses.showVotesUponReview = true;\n } else {\n this.quiz.question.isRunning = false;\n Instructor.enableControls([]);\n }\n });\n }\n\n /**\n * Show a question list dropdown.\n * @param {string} name\n */\n showQuestionListSetup(name) {\n let controlButton = Instructor.control(name);\n if (controlButton.classList.contains('active')) {\n // It's already open. Let's not send another request.\n return;\n }\n Ajax.get(`list_${name}_questions`, {}, data => {\n controlButton.classList.add('active');\n const menu = document.querySelector(`#jazzquiz_${name}_menu`);\n menu.innerHTML = '';\n menu.classList.add('active');\n const margin = controlButton.getBoundingClientRect().left - controlButton.parentElement.getBoundingClientRect().left;\n menu.style.marginLeft = margin + 'px';\n const questions = data.questions;\n for (let i in questions) {\n if (!questions.hasOwnProperty(i)) {\n continue;\n }\n let questionButton = document.createElement('BUTTON');\n questionButton.classList.add('btn');\n Quiz.addMathjaxElement(questionButton, questions[i].name);\n questionButton.dataset.time = questions[i].time;\n questionButton.dataset.questionId = questions[i].questionid;\n questionButton.dataset.jazzquizQuestionId = questions[i].jazzquizquestionid;\n questionButton.addEventListener('click', () => {\n const questionId = questionButton.dataset.questionId;\n const time = questionButton.dataset.time;\n const jazzQuestionId = questionButton.dataset.jazzquizQuestionId;\n this.jumpQuestion(questionId, time, jazzQuestionId);\n menu.innerHTML = '';\n menu.classList.remove('active');\n controlButton.classList.remove('active');\n });\n menu.appendChild(questionButton);\n }\n });\n }\n\n /**\n * Get the selected responses.\n * @returns {Array.} Vote options\n */\n static getSelectedAnswersForVote() {\n return document.querySelectorAll(selectors.quiz.selectedVoteOption).map(option => {\n return {text: option.dataset.response, count: option.dataset.count};\n });\n }\n\n /**\n * Start a vote with the responses that are currently selected.\n */\n runVoting() {\n const options = Instructor.getSelectedAnswersForVote();\n const data = {questions: encodeURIComponent(JSON.stringify(options))};\n Ajax.post('run_voting', data);\n }\n\n /**\n * Start a new question in this session.\n * @param {string} method\n * @param {number} questionId\n * @param {number} questionTime\n * @param {number} jazzquizQuestionId\n */\n startQuestion(method, questionId, questionTime, jazzquizQuestionId) {\n document.querySelector(selectors.quiz.info).classList.add('hidden');\n this.responses.clear();\n this.hideCorrectAnswer();\n Ajax.post('start_question', {\n method: method,\n questionid: questionId,\n questiontime: questionTime,\n jazzquizquestionid: jazzquizQuestionId\n }, data => this.quiz.question.startCountdown(data.questiontime, data.delay));\n }\n\n /**\n * Jump to a planned question in the quiz.\n * @param {number} questionId\n * @param {number} questionTime\n * @param {number} jazzquizQuestionId\n */\n jumpQuestion(questionId, questionTime, jazzquizQuestionId) {\n this.startQuestion('jump', questionId, questionTime, jazzquizQuestionId);\n }\n\n /**\n * Repoll the previously asked question.\n */\n repollQuestion() {\n this.startQuestion('repoll', 0, 0, 0);\n }\n\n /**\n * Continue on to the next preplanned question.\n */\n nextQuestion() {\n this.startQuestion('next', 0, 0, 0);\n }\n\n /**\n * Start a random question.\n */\n randomQuestion() {\n this.startQuestion('random', 0, 0, 0);\n }\n\n /**\n * Close the current session.\n */\n closeSession() {\n document.querySelector(selectors.quiz.undoMergeButton).classList.add('hidden');\n document.querySelector(selectors.question.box).classList.add('hidden');\n Instructor.controls.classList.add('hidden');\n setText(document.querySelector(selectors.quiz.info), 'closing_session');\n // eslint-disable-next-line no-return-assign\n Ajax.post('close_session', {}, () => window.location = location.href.split('&')[0]);\n }\n\n /**\n * Hide the correct answer if showing.\n */\n hideCorrectAnswer() {\n if (this.isShowingCorrectAnswer) {\n Instructor.correctAnswer.classList.add('hidden');\n Instructor.control('answer').querySelector('.fa').classList.replace('fa-check-square-o', 'fa-square-o');\n this.isShowingCorrectAnswer = false;\n }\n }\n\n /**\n * Request and show the correct answer for the ongoing or previous question.\n */\n showCorrectAnswer() {\n this.hideCorrectAnswer();\n Ajax.get('get_right_response', {}, data => {\n Instructor.correctAnswer.innerHTML = data.right_answer;\n Instructor.correctAnswer.classList.remove('hidden');\n Quiz.renderAllMathjax();\n Instructor.control('answer').querySelector('.fa').classList.replace('fa-square-o', 'fa-check-square-o');\n this.isShowingCorrectAnswer = true;\n });\n }\n\n /**\n * Enables all buttons passed in arguments, but disables all others.\n * @param {Array.} buttons The unique part of the IDs of the buttons to be enabled.\n */\n static enableControls(buttons) {\n document.querySelectorAll(selectors.quiz.controlButton).forEach(controlButton => {\n controlButton.disabled = (buttons.indexOf(controlButton.dataset.control) === -1);\n });\n }\n\n /**\n * Enter fullscreen mode for better use with projectors.\n */\n static showFullscreenView() {\n if (document.querySelector(selectors.main).classList.contains('jazzquiz-fullscreen')) {\n Instructor.closeFullscreenView();\n return;\n }\n // Hide the scrollbar - remember to always set back to auto when closing.\n document.documentElement.style.overflowY = 'hidden';\n // Sets the quiz view to an absolute position that covers the viewport.\n document.querySelector(selectors.main).classList.add('jazzquiz-fullscreen');\n }\n\n /**\n * Exit the fullscreen mode.\n */\n static closeFullscreenView() {\n document.documentElement.style.overflowY = 'auto';\n document.querySelector(selectors.main).classList.remove('jazzquiz-fullscreen');\n }\n\n /**\n * Close the dropdown menu for choosing a question.\n * @param {Event} event\n * @param {string} name\n */\n static closeQuestionListMenu(event, name) {\n // Close the menu if the click was not inside.\n if (!event.target.closest(`#jazzquiz_${name}_menu`)) {\n const menu = document.querySelector(`#jazzquiz_${name}_menu`);\n menu.innerHTML = '';\n menu.classList.remove('active');\n const controlButton = document.querySelector(`#jazzquiz_control_${name}`);\n if (controlButton) {\n controlButton.classList.remove('active');\n }\n }\n }\n\n static addReportEventHandlers() {\n document.addEventListener('click', event => {\n const reportOverviewControlsButton = event.target.closest('#report_overview_controls button');\n if (reportOverviewControlsButton) {\n const action = reportOverviewControlsButton.dataset.action;\n if (action === 'attendance') {\n document.querySelector(selectors.quiz.reportOverviewResponded).classList.remove('hidden');\n document.querySelector(selectors.quiz.reportOverviewResponses).classList.add('hidden');\n } else if (action === 'responses') {\n document.querySelector(selectors.quiz.reportOverviewResponses).classList.remove('hidden');\n document.querySelector(selectors.quiz.reportOverviewResponded).classList.add('hidden');\n }\n }\n });\n }\n\n}\n\nexport const initialize = (totalQuestions, reportView, slots) => {\n const quiz = new Quiz(Instructor);\n quiz.role.totalQuestions = totalQuestions;\n if (reportView) {\n Instructor.addReportEventHandlers();\n quiz.role.responses.showResponses = true;\n slots.forEach(slot => {\n const wrapper = 'jazzquiz_wrapper_responses_' + slot.num;\n const table = 'responses_wrapper_table_' + slot.num;\n const graph = 'report_' + slot.num;\n quiz.role.responses.set(wrapper, table, slot.responses, undefined, slot.type, graph, false);\n });\n } else {\n quiz.poll(500);\n }\n};\n"],"names":["ResponseView","constructor","quiz","currentResponses","showVotesUponReview","respondedCount","showResponses","totalStudents","document","addEventListener","event","target","closest","selectors","undoMergeButton","undoMerge","showNormalResultsButton","refresh","showVoteResultsButton","refreshVotes","classList","contains","startMerge","id","parentNode","clear","querySelector","responses","innerHTML","responseInfo","hide","Instructor","control","replace","add","show","remove","this","toggle","endMerge","querySelectorAll","mergeInto","forEach","element","mergeFrom","post","merge","from","into","fromRowBarId","barCell","getElementById","row","parentElement","fromRow","dataset","response","tableRow","cell","createControls","name","quizResponseInfo","question","hasVotes","state","showNormalResult","showVoteResult","html","children","addBarGraphRow","i","highestResponseCount","percent","parseInt","count","rowIndex","currentRowIndex","j","rows","length","insertRow","responseIndex","countHtml","responseCell","insertCell","onclick","latexId","addMathjaxElement","qtype","renderMaximaEquation","currentRow","containsOutside","countElement","barElement","firstElementChild","style","width","createBarGraph","targetId","graphId","rebuild","prune","deleteRow","isSorting","insertBefore","set","wrapperId","tableId","responded","questionType","undefined","quizResponded","a","b","trim","exists","push","wrapper","sortBarGraph","get","data","has_votes","total_students","question_type","merge_count","quizResponses","answers","hasOwnProperty","attempt","finalcount","slot","isShowingCorrectAnswer","totalQuestions","allowVote","key","closeFullscreenView","addEvents","repollQuestion","runVoting","showQuestionListSetup","nextQuestion","randomQuestion","endQuestion","showFullscreenView","showCorrectAnswer","closeSession","startQuiz","addHotkeys","closeQuestionListMenu","startQuizButton","exitQuizButton","keys","action","repeat","ctrlKey","toLowerCase","focusedTag","prop","preventDefault","$control","click","events","controlButton","enableControls","controls","controlsBox","controlButtons","side","sideContainer","correctAnswer","correctAnswerContainer","isMerging","onNotRunning","student_count","info","studentsJoined","studentsJoinedCounter","onPreparing","enabledButtons","onRunning","questionTime","questiontime","isRunning","delay","startCountdown","onReviewing","Question","isLoaded","isNewState","onSessionClosed","onVoting","onStateChange","onQuestionRefreshed","voteable","onTimerEnding","onTimerTick","timeLeft","timer","hideTimer","menu","margin","getBoundingClientRect","left","marginLeft","questions","questionButton","createElement","time","questionId","questionid","jazzquizQuestionId","jazzquizquestionid","jazzQuestionId","jumpQuestion","appendChild","selectedVoteOption","map","option","text","options","getSelectedAnswersForVote","encodeURIComponent","JSON","stringify","startQuestion","method","hideCorrectAnswer","box","window","location","href","split","right_answer","renderAllMathjax","buttons","disabled","indexOf","main","documentElement","overflowY","reportOverviewControlsButton","reportOverviewResponded","reportOverviewResponses","reportView","slots","Quiz","role","addReportEventHandlers","num","table","graph","type","poll"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/question.min.js b/amd/build/question.min.js
index 6a30346..6c0dcda 100644
--- a/amd/build/question.min.js
+++ b/amd/build/question.min.js
@@ -6,6 +6,6 @@ define("mod_jazzquiz/question",["exports","mod_jazzquiz/core","mod_jazzquiz/sele
* @author Sebastian S. Gundersen
* @copyright 2024 NTNU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Question=void 0,_selectors=_interopRequireDefault(_selectors);class Question{constructor(quiz){this.quiz=quiz,this.isRunning=!1,this.isSaving=!1,this.endTime=0,this.isVoteRunning=!1,this.hasVotes=!1,this.countdownTimeLeft=0,this.questionTime=0,this.countdownInterval=0,this.timerInterval=0}static get box(){return document.querySelector(_selectors.default.question.box)}static get timer(){return document.querySelector(_selectors.default.question.timer)}static get form(){return document.querySelector(_selectors.default.question.form)}refresh(){_core.Ajax.get("get_question_form",{},(data=>{data.is_already_submitted?(0,_core.setText)(_core.Quiz.info,"wait_for_instructor"):(_core.Quiz.show(Question.box.html(data.html)),eval(data.js),data.css.forEach((cssUrl=>{let head=document.getElementsByTagName("head")[0],style=document.createElement("link");style.rel="stylesheet",style.type="text/css",style.href=cssUrl,head.appendChild(style)})),void 0!==this.quiz.role.onQuestionRefreshed&&this.quiz.role.onQuestionRefreshed(data),_core.Quiz.renderAllMathjax())}))}hideTimer(){_core.Quiz.hide(Question.timer),clearInterval(this.timerInterval),this.timerInterval=0}onCountdownTick(questionTime){this.countdownTimeLeft--,this.countdownTimeLeft<=0?(clearInterval(this.countdownInterval),this.countdownInterval=0,this.startAttempt(questionTime)):0!==this.countdownTimeLeft?(0,_core.setText)(_core.Quiz.info,"question_will_start_in_x_seconds","jazzquiz",this.countdownTimeLeft):(0,_core.setText)(_core.Quiz.info,"question_will_start_now")}startCountdown(questionTime,countdownTimeLeft){return 0!==this.countdownInterval||(questionTime=parseInt(questionTime),countdownTimeLeft=parseInt(countdownTimeLeft),this.countdownTimeLeft=countdownTimeLeft,countdownTimeLeft<1?!(questionTime>0&&countdownTimeLeft<-questionTime)&&(questionTime>1?this.startAttempt(questionTime+countdownTimeLeft):this.startAttempt(0),!0):(this.countdownInterval=setInterval((()=>this.onCountdownTick(questionTime)),1e3),!0))}onTimerEnding(){this.isRunning=!1,void 0!==this.quiz.role.onTimerEnding&&this.quiz.role.onTimerEnding()}onTimerTick(){const currentTime=(new Date).getTime();if(currentTime>this.endTime)this.hideTimer(),this.onTimerEnding();else{const timeLeft=parseInt((this.endTime-currentTime)/1e3);this.quiz.role.onTimerTick(timeLeft)}}startAttempt(questionTime){_core.Quiz.hide(_core.Quiz.info),this.refresh(),this.isRunning=!0,0!==(questionTime=parseInt(questionTime))&&(this.quiz.role.onTimerTick(questionTime),this.endTime=(new Date).getTime()+1e3*questionTime,this.timerInterval=setInterval((()=>this.onTimerTick()),1e3))}static isLoaded(){return""!==Question.box.html()}}_exports.Question=Question}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Question=void 0,_selectors=_interopRequireDefault(_selectors);class Question{constructor(quiz){this.quiz=quiz,this.isRunning=!1,this.isSaving=!1,this.endTime=0,this.isVoteRunning=!1,this.hasVotes=!1,this.countdownTimeLeft=0,this.questionTime=0,this.countdownInterval=0,this.timerInterval=0}refresh(){_core.Ajax.get("get_question_form",{},(data=>{if(data.is_already_submitted)return void(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"wait_for_instructor");const questionBox=document.querySelector(_selectors.default.question.box);questionBox.innerHTML=data.html,questionBox.classList.remove("hidden"),eval(data.js),data.css.forEach((cssUrl=>{let head=document.getElementsByTagName("head")[0],style=document.createElement("link");style.rel="stylesheet",style.type="text/css",style.href=cssUrl,head.appendChild(style)})),void 0!==this.quiz.role.onQuestionRefreshed&&this.quiz.role.onQuestionRefreshed(data),_core.Quiz.renderAllMathjax()}))}hideTimer(){document.querySelector(_selectors.default.question.timer).classList.add("hidden"),clearInterval(this.timerInterval),this.timerInterval=0}onCountdownTick(questionTime){const quizInfo=document.querySelector(_selectors.default.quiz.info);this.countdownTimeLeft--,this.countdownTimeLeft<=0?(clearInterval(this.countdownInterval),this.countdownInterval=0,this.startAttempt(questionTime)):0!==this.countdownTimeLeft?(0,_core.setText)(quizInfo,"question_will_start_in_x_seconds","jazzquiz",this.countdownTimeLeft):(0,_core.setText)(quizInfo,"question_will_start_now")}startCountdown(questionTime,countdownTimeLeft){return 0!==this.countdownInterval||(questionTime=parseInt(questionTime),countdownTimeLeft=parseInt(countdownTimeLeft),this.countdownTimeLeft=countdownTimeLeft,countdownTimeLeft<1?!(questionTime>0&&countdownTimeLeft<-questionTime)&&(questionTime>1?this.startAttempt(questionTime+countdownTimeLeft):this.startAttempt(0),!0):(this.countdownInterval=setInterval((()=>this.onCountdownTick(questionTime)),1e3),!0))}onTimerEnding(){this.isRunning=!1,void 0!==this.quiz.role.onTimerEnding&&this.quiz.role.onTimerEnding()}onTimerTick(){const currentTime=(new Date).getTime();if(currentTime>this.endTime)this.hideTimer(),this.onTimerEnding();else{const timeLeft=parseInt((this.endTime-currentTime)/1e3);this.quiz.role.onTimerTick(timeLeft)}}startAttempt(questionTime){document.querySelector(_selectors.default.quiz.info).classList.add("hidden"),this.refresh(),this.isRunning=!0,0!==(questionTime=parseInt(questionTime))&&(this.quiz.role.onTimerTick(questionTime),this.endTime=(new Date).getTime()+1e3*questionTime,this.timerInterval=setInterval((()=>this.onTimerTick()),1e3))}static isLoaded(){return document.querySelector(_selectors.default.question.box).innerHTML.length>0}}_exports.Question=Question}));
//# sourceMappingURL=question.min.js.map
\ No newline at end of file
diff --git a/amd/build/question.min.js.map b/amd/build/question.min.js.map
index f3d5ccf..f875eb2 100644
--- a/amd/build/question.min.js.map
+++ b/amd/build/question.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {Quiz, Ajax, setText} from 'mod_jazzquiz/core';\nimport selectors from 'mod_jazzquiz/selectors';\n\n/**\n * Current question.\n *\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2024 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport class Question {\n\n constructor(quiz) {\n this.quiz = quiz;\n this.isRunning = false;\n this.isSaving = false;\n this.endTime = 0;\n this.isVoteRunning = false;\n this.hasVotes = false;\n this.countdownTimeLeft = 0;\n this.questionTime = 0;\n this.countdownInterval = 0;\n this.timerInterval = 0;\n }\n\n static get box() {\n return document.querySelector(selectors.question.box);\n }\n\n static get timer() {\n return document.querySelector(selectors.question.timer);\n }\n\n static get form() {\n return document.querySelector(selectors.question.form);\n }\n\n /**\n * Request the current question form.\n */\n refresh() {\n Ajax.get('get_question_form', {}, data => {\n if (data.is_already_submitted) {\n setText(Quiz.info, 'wait_for_instructor');\n return;\n }\n Quiz.show(Question.box.html(data.html));\n // eslint-disable-next-line no-eval\n eval(data.js);\n data.css.forEach(cssUrl => {\n let head = document.getElementsByTagName('head')[0];\n let style = document.createElement('link');\n style.rel = 'stylesheet';\n style.type = 'text/css';\n style.href = cssUrl;\n head.appendChild(style);\n });\n if (this.quiz.role.onQuestionRefreshed !== undefined) {\n this.quiz.role.onQuestionRefreshed(data);\n }\n Quiz.renderAllMathjax();\n });\n }\n\n /**\n * Hide the question \"ending in\" timer, and clears the interval.\n */\n hideTimer() {\n Quiz.hide(Question.timer);\n clearInterval(this.timerInterval);\n this.timerInterval = 0;\n }\n\n /**\n * Is called for every second of the question countdown.\n * @param {number} questionTime in seconds\n */\n onCountdownTick(questionTime) {\n this.countdownTimeLeft--;\n if (this.countdownTimeLeft <= 0) {\n clearInterval(this.countdownInterval);\n this.countdownInterval = 0;\n this.startAttempt(questionTime);\n } else if (this.countdownTimeLeft !== 0) {\n setText(Quiz.info, 'question_will_start_in_x_seconds', 'jazzquiz', this.countdownTimeLeft);\n } else {\n setText(Quiz.info, 'question_will_start_now');\n }\n }\n\n /**\n * Start a countdown for the question which will eventually start the question attempt.\n * The question attempt might start before this function return, depending on the arguments.\n * If a countdown has already been started, this call will return true and the current countdown will continue.\n * @param {number} questionTime\n * @param {number} countdownTimeLeft\n * @return {boolean} true if countdown is active\n */\n startCountdown(questionTime, countdownTimeLeft) {\n if (this.countdownInterval !== 0) {\n return true;\n }\n questionTime = parseInt(questionTime);\n countdownTimeLeft = parseInt(countdownTimeLeft);\n this.countdownTimeLeft = countdownTimeLeft;\n if (countdownTimeLeft < 1) {\n // Check if the question has already ended.\n if (questionTime > 0 && countdownTimeLeft < -questionTime) {\n return false;\n }\n // No need to start the countdown. Just start the question.\n if (questionTime > 1) {\n this.startAttempt(questionTime + countdownTimeLeft);\n } else {\n this.startAttempt(0);\n }\n return true;\n }\n this.countdownInterval = setInterval(() => this.onCountdownTick(questionTime), 1000);\n return true;\n }\n\n /**\n * When the question \"ending in\" timer reaches 0 seconds, this will be called.\n */\n onTimerEnding() {\n this.isRunning = false;\n if (this.quiz.role.onTimerEnding !== undefined) {\n this.quiz.role.onTimerEnding();\n }\n }\n\n /**\n * Is called for every second of the \"ending in\" timer.\n */\n onTimerTick() {\n const currentTime = new Date().getTime();\n if (currentTime > this.endTime) {\n this.hideTimer();\n this.onTimerEnding();\n } else {\n const timeLeft = parseInt((this.endTime - currentTime) / 1000);\n this.quiz.role.onTimerTick(timeLeft);\n }\n }\n\n /**\n * Request the current question from the server.\n * @param {number} questionTime\n */\n startAttempt(questionTime) {\n Quiz.hide(Quiz.info);\n this.refresh();\n // Set this to true so that we don't keep calling this over and over.\n this.isRunning = true;\n questionTime = parseInt(questionTime);\n if (questionTime === 0) {\n // 0 means no timer.\n return;\n }\n this.quiz.role.onTimerTick(questionTime); // TODO: Is it worth having this line?\n this.endTime = new Date().getTime() + questionTime * 1000;\n this.timerInterval = setInterval(() => this.onTimerTick(), 1000);\n }\n\n static isLoaded() {\n return Question.box.html() !== '';\n }\n\n}\n"],"names":["Question","constructor","quiz","isRunning","isSaving","endTime","isVoteRunning","hasVotes","countdownTimeLeft","questionTime","countdownInterval","timerInterval","box","document","querySelector","selectors","question","timer","form","refresh","get","data","is_already_submitted","Quiz","info","show","html","eval","js","css","forEach","cssUrl","head","getElementsByTagName","style","createElement","rel","type","href","appendChild","undefined","this","role","onQuestionRefreshed","renderAllMathjax","hideTimer","hide","clearInterval","onCountdownTick","startAttempt","startCountdown","parseInt","setInterval","onTimerEnding","onTimerTick","currentTime","Date","getTime","timeLeft"],"mappings":";;;;;;;;0IA2BaA,SAETC,YAAYC,WACHA,KAAOA,UACPC,WAAY,OACZC,UAAW,OACXC,QAAU,OACVC,eAAgB,OAChBC,UAAW,OACXC,kBAAoB,OACpBC,aAAe,OACfC,kBAAoB,OACpBC,cAAgB,EAGdC,wBACAC,SAASC,cAAcC,mBAAUC,SAASJ,KAG1CK,0BACAJ,SAASC,cAAcC,mBAAUC,SAASC,OAG1CC,yBACAL,SAASC,cAAcC,mBAAUC,SAASE,MAMrDC,qBACSC,IAAI,oBAAqB,IAAIC,OAC1BA,KAAKC,uCACGC,WAAKC,KAAM,mCAGlBC,KAAKzB,SAASY,IAAIc,KAAKL,KAAKK,OAEjCC,KAAKN,KAAKO,IACVP,KAAKQ,IAAIC,SAAQC,aACTC,KAAOnB,SAASoB,qBAAqB,QAAQ,GAC7CC,MAAQrB,SAASsB,cAAc,QACnCD,MAAME,IAAM,aACZF,MAAMG,KAAO,WACbH,MAAMI,KAAOP,OACbC,KAAKO,YAAYL,eAEsBM,IAAvCC,KAAKvC,KAAKwC,KAAKC,0BACVzC,KAAKwC,KAAKC,oBAAoBtB,iBAElCuB,uBAObC,uBACSC,KAAK9C,SAASiB,OACnB8B,cAAcN,KAAK9B,oBACdA,cAAgB,EAOzBqC,gBAAgBvC,mBACPD,oBACDiC,KAAKjC,mBAAqB,GAC1BuC,cAAcN,KAAK/B,wBACdA,kBAAoB,OACpBuC,aAAaxC,eACgB,IAA3BgC,KAAKjC,oCACJe,WAAKC,KAAM,mCAAoC,WAAYiB,KAAKjC,qCAEhEe,WAAKC,KAAM,2BAY3B0B,eAAezC,aAAcD,0BACM,IAA3BiC,KAAK/B,oBAGTD,aAAe0C,SAAS1C,cACxBD,kBAAoB2C,SAAS3C,wBACxBA,kBAAoBA,kBACrBA,kBAAoB,IAEhBC,aAAe,GAAKD,mBAAqBC,gBAIzCA,aAAe,OACVwC,aAAaxC,aAAeD,wBAE5ByC,aAAa,IAEf,SAENvC,kBAAoB0C,aAAY,IAAMX,KAAKO,gBAAgBvC,eAAe,MACxE,IAMX4C,qBACSlD,WAAY,OACoBqC,IAAjCC,KAAKvC,KAAKwC,KAAKW,oBACVnD,KAAKwC,KAAKW,gBAOvBC,oBACUC,aAAc,IAAIC,MAAOC,aAC3BF,YAAcd,KAAKpC,aACdwC,iBACAQ,oBACF,OACGK,SAAWP,UAAUV,KAAKpC,QAAUkD,aAAe,UACpDrD,KAAKwC,KAAKY,YAAYI,WAQnCT,aAAaxC,yBACJqC,KAAKvB,WAAKC,WACVL,eAEAhB,WAAY,EAEI,KADrBM,aAAe0C,SAAS1C,sBAKnBP,KAAKwC,KAAKY,YAAY7C,mBACtBJ,SAAU,IAAImD,MAAOC,UAA2B,IAAfhD,kBACjCE,cAAgByC,aAAY,IAAMX,KAAKa,eAAe,8BAI5B,KAAxBtD,SAASY,IAAIc"}
\ No newline at end of file
+{"version":3,"file":"question.min.js","sources":["../src/question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {Quiz, Ajax, setText} from 'mod_jazzquiz/core';\nimport selectors from 'mod_jazzquiz/selectors';\n\n/**\n * Current question.\n *\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2024 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport class Question {\n\n constructor(quiz) {\n this.quiz = quiz;\n this.isRunning = false;\n this.isSaving = false;\n this.endTime = 0;\n this.isVoteRunning = false;\n this.hasVotes = false;\n this.countdownTimeLeft = 0;\n this.questionTime = 0;\n this.countdownInterval = 0;\n this.timerInterval = 0;\n }\n\n /**\n * Request the current question form.\n */\n refresh() {\n Ajax.get('get_question_form', {}, data => {\n if (data.is_already_submitted) {\n setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');\n return;\n }\n const questionBox = document.querySelector(selectors.question.box);\n questionBox.innerHTML = data.html;\n questionBox.classList.remove('hidden');\n // eslint-disable-next-line no-eval\n eval(data.js);\n data.css.forEach(cssUrl => {\n let head = document.getElementsByTagName('head')[0];\n let style = document.createElement('link');\n style.rel = 'stylesheet';\n style.type = 'text/css';\n style.href = cssUrl;\n head.appendChild(style);\n });\n if (this.quiz.role.onQuestionRefreshed !== undefined) {\n this.quiz.role.onQuestionRefreshed(data);\n }\n Quiz.renderAllMathjax();\n });\n }\n\n /**\n * Hide the question \"ending in\" timer, and clears the interval.\n */\n hideTimer() {\n document.querySelector(selectors.question.timer).classList.add('hidden');\n clearInterval(this.timerInterval);\n this.timerInterval = 0;\n }\n\n /**\n * Is called for every second of the question countdown.\n * @param {number} questionTime in seconds\n */\n onCountdownTick(questionTime) {\n const quizInfo = document.querySelector(selectors.quiz.info);\n this.countdownTimeLeft--;\n if (this.countdownTimeLeft <= 0) {\n clearInterval(this.countdownInterval);\n this.countdownInterval = 0;\n this.startAttempt(questionTime);\n } else if (this.countdownTimeLeft !== 0) {\n setText(quizInfo, 'question_will_start_in_x_seconds', 'jazzquiz', this.countdownTimeLeft);\n } else {\n setText(quizInfo, 'question_will_start_now');\n }\n }\n\n /**\n * Start a countdown for the question which will eventually start the question attempt.\n * The question attempt might start before this function return, depending on the arguments.\n * If a countdown has already been started, this call will return true and the current countdown will continue.\n * @param {number} questionTime\n * @param {number} countdownTimeLeft\n * @return {boolean} true if countdown is active\n */\n startCountdown(questionTime, countdownTimeLeft) {\n if (this.countdownInterval !== 0) {\n return true;\n }\n questionTime = parseInt(questionTime);\n countdownTimeLeft = parseInt(countdownTimeLeft);\n this.countdownTimeLeft = countdownTimeLeft;\n if (countdownTimeLeft < 1) {\n // Check if the question has already ended.\n if (questionTime > 0 && countdownTimeLeft < -questionTime) {\n return false;\n }\n // No need to start the countdown. Just start the question.\n if (questionTime > 1) {\n this.startAttempt(questionTime + countdownTimeLeft);\n } else {\n this.startAttempt(0);\n }\n return true;\n }\n this.countdownInterval = setInterval(() => this.onCountdownTick(questionTime), 1000);\n return true;\n }\n\n /**\n * When the question \"ending in\" timer reaches 0 seconds, this will be called.\n */\n onTimerEnding() {\n this.isRunning = false;\n if (this.quiz.role.onTimerEnding !== undefined) {\n this.quiz.role.onTimerEnding();\n }\n }\n\n /**\n * Is called for every second of the \"ending in\" timer.\n */\n onTimerTick() {\n const currentTime = new Date().getTime();\n if (currentTime > this.endTime) {\n this.hideTimer();\n this.onTimerEnding();\n } else {\n const timeLeft = parseInt((this.endTime - currentTime) / 1000);\n this.quiz.role.onTimerTick(timeLeft);\n }\n }\n\n /**\n * Request the current question from the server.\n * @param {number} questionTime\n */\n startAttempt(questionTime) {\n document.querySelector(selectors.quiz.info).classList.add('hidden');\n this.refresh();\n // Set this to true so that we don't keep calling this over and over.\n this.isRunning = true;\n questionTime = parseInt(questionTime);\n if (questionTime === 0) {\n // 0 means no timer.\n return;\n }\n this.quiz.role.onTimerTick(questionTime); // TODO: Is it worth having this line?\n this.endTime = new Date().getTime() + questionTime * 1000;\n this.timerInterval = setInterval(() => this.onTimerTick(), 1000);\n }\n\n static isLoaded() {\n return document.querySelector(selectors.question.box).innerHTML.length > 0;\n }\n\n}\n"],"names":["Question","constructor","quiz","isRunning","isSaving","endTime","isVoteRunning","hasVotes","countdownTimeLeft","questionTime","countdownInterval","timerInterval","refresh","get","data","is_already_submitted","document","querySelector","selectors","info","questionBox","question","box","innerHTML","html","classList","remove","eval","js","css","forEach","cssUrl","head","getElementsByTagName","style","createElement","rel","type","href","appendChild","undefined","this","role","onQuestionRefreshed","renderAllMathjax","hideTimer","timer","add","clearInterval","onCountdownTick","quizInfo","startAttempt","startCountdown","parseInt","setInterval","onTimerEnding","onTimerTick","currentTime","Date","getTime","timeLeft","length"],"mappings":";;;;;;;;0IA2BaA,SAETC,YAAYC,WACHA,KAAOA,UACPC,WAAY,OACZC,UAAW,OACXC,QAAU,OACVC,eAAgB,OAChBC,UAAW,OACXC,kBAAoB,OACpBC,aAAe,OACfC,kBAAoB,OACpBC,cAAgB,EAMzBC,qBACSC,IAAI,oBAAqB,IAAIC,UAC1BA,KAAKC,kDACGC,SAASC,cAAcC,mBAAUhB,KAAKiB,MAAO,6BAGnDC,YAAcJ,SAASC,cAAcC,mBAAUG,SAASC,KAC9DF,YAAYG,UAAYT,KAAKU,KAC7BJ,YAAYK,UAAUC,OAAO,UAE7BC,KAAKb,KAAKc,IACVd,KAAKe,IAAIC,SAAQC,aACTC,KAAOhB,SAASiB,qBAAqB,QAAQ,GAC7CC,MAAQlB,SAASmB,cAAc,QACnCD,MAAME,IAAM,aACZF,MAAMG,KAAO,WACbH,MAAMI,KAAOP,OACbC,KAAKO,YAAYL,eAEsBM,IAAvCC,KAAKvC,KAAKwC,KAAKC,0BACVzC,KAAKwC,KAAKC,oBAAoB7B,iBAElC8B,sBAObC,YACI7B,SAASC,cAAcC,mBAAUG,SAASyB,OAAOrB,UAAUsB,IAAI,UAC/DC,cAAcP,KAAK9B,oBACdA,cAAgB,EAOzBsC,gBAAgBxC,oBACNyC,SAAWlC,SAASC,cAAcC,mBAAUhB,KAAKiB,WAClDX,oBACDiC,KAAKjC,mBAAqB,GAC1BwC,cAAcP,KAAK/B,wBACdA,kBAAoB,OACpByC,aAAa1C,eACgB,IAA3BgC,KAAKjC,oCACJ0C,SAAU,mCAAoC,WAAYT,KAAKjC,qCAE/D0C,SAAU,2BAY1BE,eAAe3C,aAAcD,0BACM,IAA3BiC,KAAK/B,oBAGTD,aAAe4C,SAAS5C,cACxBD,kBAAoB6C,SAAS7C,wBACxBA,kBAAoBA,kBACrBA,kBAAoB,IAEhBC,aAAe,GAAKD,mBAAqBC,gBAIzCA,aAAe,OACV0C,aAAa1C,aAAeD,wBAE5B2C,aAAa,IAEf,SAENzC,kBAAoB4C,aAAY,IAAMb,KAAKQ,gBAAgBxC,eAAe,MACxE,IAMX8C,qBACSpD,WAAY,OACoBqC,IAAjCC,KAAKvC,KAAKwC,KAAKa,oBACVrD,KAAKwC,KAAKa,gBAOvBC,oBACUC,aAAc,IAAIC,MAAOC,aAC3BF,YAAchB,KAAKpC,aACdwC,iBACAU,oBACF,OACGK,SAAWP,UAAUZ,KAAKpC,QAAUoD,aAAe,UACpDvD,KAAKwC,KAAKc,YAAYI,WAQnCT,aAAa1C,cACTO,SAASC,cAAcC,mBAAUhB,KAAKiB,MAAMM,UAAUsB,IAAI,eACrDnC,eAEAT,WAAY,EAEI,KADrBM,aAAe4C,SAAS5C,sBAKnBP,KAAKwC,KAAKc,YAAY/C,mBACtBJ,SAAU,IAAIqD,MAAOC,UAA2B,IAAflD,kBACjCE,cAAgB2C,aAAY,IAAMb,KAAKe,eAAe,+BAIpDxC,SAASC,cAAcC,mBAAUG,SAASC,KAAKC,UAAUsC,OAAS"}
\ No newline at end of file
diff --git a/amd/build/selectors.min.js b/amd/build/selectors.min.js
index 8d716b6..8adb81f 100644
--- a/amd/build/selectors.min.js
+++ b/amd/build/selectors.min.js
@@ -1,3 +1,3 @@
-define("mod_jazzquiz/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={main:"#jazzquiz",question:{box:"#jazzquiz_question_box",timer:"#jazzquiz_question_timer",form:"#jazzquiz_question_form"},quiz:{info:"#jazzquiz_info_container",responded:"#jazzquiz_responded_container",responses:"#jazzquiz_responses_container",responseInfo:"#jazzquiz_response_info_container",undoMergeButton:"#jazzquiz_undo_merge",showNormalResultsButton:"#review_show_normal_results",showVoteResultsButton:"#review_show_vote_results",mergeInto:".merge-into",mergeFrom:".merge-from"},edit:{addSelectedQuestions:".jazzquiz-add-selected-questions",questionCheckedCheckbox:"#categoryquestions td input[type=checkbox]:checked",editQuestionAction:".edit-question-action"}},_exports.default}));
+define("mod_jazzquiz/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={main:"#jazzquiz",question:{box:"#jazzquiz_question_box",timer:"#jazzquiz_question_timer",form:"#jazzquiz_question_form"},quiz:{info:"#jazzquiz_info_container",responded:"#jazzquiz_responded_container",responses:"#jazzquiz_responses_container",responseInfo:"#jazzquiz_response_info_container",undoMergeButton:"#jazzquiz_undo_merge",showNormalResultsButton:"#review_show_normal_results",showVoteResultsButton:"#review_show_vote_results",mergeInto:".merge-into",mergeFrom:".merge-from",saveVoteButton:"#jazzquiz_save_vote",selectVoteButton:".jazzquiz-select-vote",controls:"#jazzquiz_controls",controlsBox:"#jazzquiz_controls_box",controlButtons:".quiz-control-buttons",sideContainer:"#jazzquiz_side_container",correctAnswerContainer:"#jazzquiz_correct_answer_container",controlButton:".jazzquiz-control-button",selectedVoteOption:".selected-vote-option",reportOverviewResponded:"#report_overview_responded",reportOverviewResponses:"#report_overview_responses",studentsJoinedCounter:"#jazzquiz_students_joined_counter",startQuizButton:"#jazzquiz_start_quiz_button",exitQuizButton:"#jazzquiz_exit_button"},edit:{addSelectedQuestions:".jazzquiz-add-selected-questions",questionCheckedCheckbox:"#categoryquestions td input[type=checkbox]:checked",editQuestionAction:".edit-question-action"}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
\ No newline at end of file
diff --git a/amd/build/selectors.min.js.map b/amd/build/selectors.min.js.map
index a46d58b..1da0253 100644
--- a/amd/build/selectors.min.js.map
+++ b/amd/build/selectors.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Selectors.\n *\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2024 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n main: '#jazzquiz',\n question: {\n box: '#jazzquiz_question_box',\n timer: '#jazzquiz_question_timer',\n form: '#jazzquiz_question_form',\n },\n quiz: {\n info: '#jazzquiz_info_container',\n responded: '#jazzquiz_responded_container',\n responses: '#jazzquiz_responses_container',\n responseInfo: '#jazzquiz_response_info_container',\n undoMergeButton: '#jazzquiz_undo_merge',\n showNormalResultsButton: '#review_show_normal_results',\n showVoteResultsButton: '#review_show_vote_results',\n mergeInto: '.merge-into',\n mergeFrom: '.merge-from',\n },\n edit: {\n addSelectedQuestions: '.jazzquiz-add-selected-questions',\n questionCheckedCheckbox: '#categoryquestions td input[type=checkbox]:checked',\n editQuestionAction: '.edit-question-action',\n },\n};\n"],"names":["main","question","box","timer","form","quiz","info","responded","responses","responseInfo","undoMergeButton","showNormalResultsButton","showVoteResultsButton","mergeInto","mergeFrom","edit","addSelectedQuestions","questionCheckedCheckbox","editQuestionAction"],"mappings":"wKAwBe,CACXA,KAAM,YACNC,SAAU,CACNC,IAAK,yBACLC,MAAO,2BACPC,KAAM,2BAEVC,KAAM,CACFC,KAAM,2BACNC,UAAW,gCACXC,UAAW,gCACXC,aAAc,oCACdC,gBAAiB,uBACjBC,wBAAyB,8BACzBC,sBAAuB,4BACvBC,UAAW,cACXC,UAAW,eAEfC,KAAM,CACFC,qBAAsB,mCACtBC,wBAAyB,qDACzBC,mBAAoB"}
\ No newline at end of file
+{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Selectors.\n *\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2024 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n main: '#jazzquiz',\n question: {\n box: '#jazzquiz_question_box',\n timer: '#jazzquiz_question_timer',\n form: '#jazzquiz_question_form',\n },\n quiz: {\n info: '#jazzquiz_info_container',\n responded: '#jazzquiz_responded_container',\n responses: '#jazzquiz_responses_container',\n responseInfo: '#jazzquiz_response_info_container',\n undoMergeButton: '#jazzquiz_undo_merge',\n showNormalResultsButton: '#review_show_normal_results',\n showVoteResultsButton: '#review_show_vote_results',\n mergeInto: '.merge-into',\n mergeFrom: '.merge-from',\n saveVoteButton: '#jazzquiz_save_vote',\n selectVoteButton: '.jazzquiz-select-vote',\n controls: '#jazzquiz_controls',\n controlsBox: '#jazzquiz_controls_box',\n controlButtons: '.quiz-control-buttons',\n sideContainer: '#jazzquiz_side_container',\n correctAnswerContainer: '#jazzquiz_correct_answer_container',\n controlButton: '.jazzquiz-control-button',\n selectedVoteOption: '.selected-vote-option',\n reportOverviewResponded: '#report_overview_responded',\n reportOverviewResponses: '#report_overview_responses',\n studentsJoinedCounter: '#jazzquiz_students_joined_counter',\n startQuizButton: '#jazzquiz_start_quiz_button',\n exitQuizButton: '#jazzquiz_exit_button',\n },\n edit: {\n addSelectedQuestions: '.jazzquiz-add-selected-questions',\n questionCheckedCheckbox: '#categoryquestions td input[type=checkbox]:checked',\n editQuestionAction: '.edit-question-action',\n },\n};\n"],"names":["main","question","box","timer","form","quiz","info","responded","responses","responseInfo","undoMergeButton","showNormalResultsButton","showVoteResultsButton","mergeInto","mergeFrom","saveVoteButton","selectVoteButton","controls","controlsBox","controlButtons","sideContainer","correctAnswerContainer","controlButton","selectedVoteOption","reportOverviewResponded","reportOverviewResponses","studentsJoinedCounter","startQuizButton","exitQuizButton","edit","addSelectedQuestions","questionCheckedCheckbox","editQuestionAction"],"mappings":"wKAwBe,CACXA,KAAM,YACNC,SAAU,CACNC,IAAK,yBACLC,MAAO,2BACPC,KAAM,2BAEVC,KAAM,CACFC,KAAM,2BACNC,UAAW,gCACXC,UAAW,gCACXC,aAAc,oCACdC,gBAAiB,uBACjBC,wBAAyB,8BACzBC,sBAAuB,4BACvBC,UAAW,cACXC,UAAW,cACXC,eAAgB,sBAChBC,iBAAkB,wBAClBC,SAAU,qBACVC,YAAa,yBACbC,eAAgB,wBAChBC,cAAe,2BACfC,uBAAwB,qCACxBC,cAAe,2BACfC,mBAAoB,wBACpBC,wBAAyB,6BACzBC,wBAAyB,6BACzBC,sBAAuB,oCACvBC,gBAAiB,8BACjBC,eAAgB,yBAEpBC,KAAM,CACFC,qBAAsB,mCACtBC,wBAAyB,qDACzBC,mBAAoB"}
\ No newline at end of file
diff --git a/amd/build/student.min.js b/amd/build/student.min.js
index 50a3902..96b0de8 100644
--- a/amd/build/student.min.js
+++ b/amd/build/student.min.js
@@ -1,10 +1,10 @@
-define("mod_jazzquiz/student",["exports","jquery","mod_jazzquiz/core"],(function(_exports,_jquery,_core){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+define("mod_jazzquiz/student",["exports","mod_jazzquiz/core","mod_jazzquiz/selectors"],(function(_exports,_core,_selectors){var obj;
/**
* @module mod_jazzquiz
* @author Sebastian S. Gundersen
* @copyright 2014 University of Wisconsin - Madison
* @copyright 2018 NTNU
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initialize=function(){new Quiz(Student).poll(2e3)},_jquery=_interopRequireDefault(_jquery);const Quiz=(_core=_interopRequireDefault(_core)).default.Quiz,Question=_core.default.Question,Ajax=_core.default.Ajax,setText=_core.default.setText;class Student{constructor(quiz){this.quiz=quiz,this.voteAnswer=void 0;let self=this;(0,_jquery.default)(document).on("submit","#jazzquiz_question_form",(function(event){event.preventDefault(),self.submitAnswer()})).on("click","#jazzquiz_save_vote",(function(){self.saveVote()})).on("click",".jazzquiz-select-vote",(function(){self.voteAnswer=this.value}))}onNotRunning(){setText(Quiz.info,"instructions_for_student")}onPreparing(){setText(Quiz.info,"wait_for_instructor")}onRunning(data){if(this.quiz.question.isRunning)return;this.quiz.question.startCountdown(data.questiontime,data.delay)||setText(Quiz.info,"wait_for_instructor")}onReviewing(){this.quiz.question.isVoteRunning=!1,this.quiz.question.isRunning=!1,this.quiz.question.hideTimer(),Quiz.hide(Question.box),setText(Quiz.info,"wait_for_instructor")}onSessionClosed(){window.location=location.href.split("&")[0]}onVoting(data){if(this.quiz.question.isVoteRunning)return;Quiz.info.html(data.html),Quiz.show(Quiz.info);const options=data.options;for(let i=0;i{data.feedback.length>0?Quiz.show(Quiz.info.html(data.feedback)):setText(Quiz.info,"wait_for_instructor"),this.quiz.question.isSaving=!1,this.quiz.question.isRunning&&(this.quiz.question.isVoteRunning||(Quiz.hide(Question.box),this.quiz.question.hideTimer()))})).fail((()=>{this.quiz.question.isSaving=!1}))}saveVote(){Ajax.post("save_vote",{vote:this.voteAnswer},(data=>{"success"===data.status?setText(Quiz.info,"wait_for_instructor"):setText(Quiz.info,"you_already_voted")}))}}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initialize=function(){new _core.Quiz(Student).poll(2e3)},_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};class Student{constructor(quiz){this.quiz=quiz,this.voteAnswer=void 0,document.addEventListener("submit",(event=>{event.target.closest(_selectors.default.question.form)&&(event.preventDefault(),this.submitAnswer())})),document.addEventListener("click",(event=>{event.target.closest(_selectors.default.quiz.saveVoteButton)&&this.saveVote();const selectVoteButton=event.target.closest(_selectors.default.quiz.selectVoteButton);selectVoteButton&&(this.voteAnswer=selectVoteButton.value)}))}onNotRunning(){(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"instructions_for_student")}onPreparing(){(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"wait_for_instructor")}onRunning(data){if(this.quiz.question.isRunning)return;this.quiz.question.startCountdown(data.questiontime,data.delay)||(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"wait_for_instructor")}onReviewing(){this.quiz.question.isVoteRunning=!1,this.quiz.question.isRunning=!1,this.quiz.question.hideTimer(),document.querySelector(_selectors.default.question.box).classList.add("hidden"),(0,_core.setText)(document.querySelector(_selectors.default.quiz.info),"wait_for_instructor")}onSessionClosed(){window.location=location.href.split("&")[0]}onVoting(data){if(this.quiz.question.isVoteRunning)return;const quizInfo=document.querySelector(_selectors.default.quiz.info);quizInfo.innerHTML=data.html,quizInfo.classList.remove("hidden");const options=data.options;for(let i=0;i{const quizInfo=document.querySelector(_selectors.default.quiz.info);data.feedback.length>0?(quizInfo.innerHTML=data.feedback,quizInfo.classList.remove("hidden")):(0,_core.setText)(quizInfo,"wait_for_instructor"),this.quiz.question.isSaving=!1,this.quiz.question.isRunning&&(this.quiz.question.isVoteRunning||(document.querySelector(_selectors.default.question.box).classList.add("hidden"),this.quiz.question.hideTimer()))})).fail((()=>{this.quiz.question.isSaving=!1}))}saveVote(){_core.Ajax.post("save_vote",{vote:this.voteAnswer},(data=>{const quizInfo=document.querySelector(_selectors.default.quiz.info);"success"===data.status?(0,_core.setText)(quizInfo,"wait_for_instructor"):(0,_core.setText)(quizInfo,"you_already_voted")}))}}}));
//# sourceMappingURL=student.min.js.map
\ No newline at end of file
diff --git a/amd/build/student.min.js.map b/amd/build/student.min.js.map
index 7317b7c..951f39d 100644
--- a/amd/build/student.min.js.map
+++ b/amd/build/student.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"student.min.js","sources":["../src/student.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Jazz from 'mod_jazzquiz/core';\n\nconst Quiz = Jazz.Quiz;\nconst Question = Jazz.Question;\nconst Ajax = Jazz.Ajax;\nconst setText = Jazz.setText;\n\nclass Student {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.voteAnswer = undefined;\n let self = this;\n $(document).on('submit', '#jazzquiz_question_form', function(event) {\n event.preventDefault();\n self.submitAnswer();\n }).on('click', '#jazzquiz_save_vote', function() {\n self.saveVote();\n }).on('click', '.jazzquiz-select-vote', function() {\n self.voteAnswer = this.value;\n });\n }\n\n onNotRunning() {\n setText(Quiz.info, 'instructions_for_student');\n }\n\n onPreparing() {\n setText(Quiz.info, 'wait_for_instructor');\n }\n\n onRunning(data) {\n if (this.quiz.question.isRunning) {\n return;\n }\n const started = this.quiz.question.startCountdown(data.questiontime, data.delay);\n if (!started) {\n setText(Quiz.info, 'wait_for_instructor');\n }\n }\n\n onReviewing() {\n this.quiz.question.isVoteRunning = false;\n this.quiz.question.isRunning = false;\n this.quiz.question.hideTimer();\n Quiz.hide(Question.box);\n setText(Quiz.info, 'wait_for_instructor');\n }\n\n onSessionClosed() {\n window.location = location.href.split('&')[0];\n }\n\n onVoting(data) {\n if (this.quiz.question.isVoteRunning) {\n return;\n }\n Quiz.info.html(data.html);\n Quiz.show(Quiz.info);\n const options = data.options;\n for (let i = 0; i < options.length; i++) {\n Quiz.addMathjaxElement($('#' + options[i].content_id), options[i].text);\n if (options[i].question_type === 'stack') {\n Quiz.renderMaximaEquation(options[i].text, options[i].content_id);\n }\n }\n this.quiz.question.isVoteRunning = true;\n }\n\n onTimerTick(timeLeft) {\n setText(Question.timer, 'question_will_end_in_x_seconds', 'jazzquiz', timeLeft);\n }\n\n /**\n * Submit answer for the current question.\n */\n submitAnswer() {\n if (this.quiz.question.isSaving) {\n // Don't save twice.\n return;\n }\n this.quiz.question.isSaving = true;\n if (typeof tinyMCE !== 'undefined') {\n // eslint-disable-next-line no-undef\n tinyMCE.triggerSave();\n }\n const serialized = Question.form.serializeArray();\n let data = {};\n for (let name in serialized) {\n if (serialized.hasOwnProperty(name)) {\n data[serialized[name].name] = serialized[name].value;\n }\n }\n Ajax.post('save_question', data, data => {\n if (data.feedback.length > 0) {\n Quiz.show(Quiz.info.html(data.feedback));\n } else {\n setText(Quiz.info, 'wait_for_instructor');\n }\n this.quiz.question.isSaving = false;\n if (!this.quiz.question.isRunning) {\n return;\n }\n if (this.quiz.question.isVoteRunning) {\n return;\n }\n Quiz.hide(Question.box);\n this.quiz.question.hideTimer();\n }).fail(() => {\n this.quiz.question.isSaving = false;\n });\n }\n\n saveVote() {\n Ajax.post('save_vote', {vote: this.voteAnswer}, data => {\n if (data.status === 'success') {\n setText(Quiz.info, 'wait_for_instructor');\n } else {\n setText(Quiz.info, 'you_already_voted');\n }\n });\n }\n\n}\n\n/**\n * Initialize student session.\n */\nexport function initialize() {\n let quiz = new Quiz(Student);\n quiz.poll(2000);\n}\n"],"names":["Quiz","Student","poll","Question","Jazz","Ajax","setText","constructor","quiz","voteAnswer","undefined","self","this","document","on","event","preventDefault","submitAnswer","saveVote","value","onNotRunning","info","onPreparing","onRunning","data","question","isRunning","startCountdown","questiontime","delay","onReviewing","isVoteRunning","hideTimer","hide","box","onSessionClosed","window","location","href","split","onVoting","html","show","options","i","length","addMathjaxElement","content_id","text","question_type","renderMaximaEquation","onTimerTick","timeLeft","timer","isSaving","tinyMCE","triggerSave","serialized","form","serializeArray","name","hasOwnProperty","post","feedback","fail","vote","status"],"mappings":";;;;;;;4FA4Je,IAAIA,KAAKC,SACfC,KAAK,oDAnIRF,mDAAYA,KACZG,SAAWC,cAAKD,SAChBE,KAAOD,cAAKC,KACZC,QAAUF,cAAKE,cAEfL,QAKFM,YAAYC,WACHA,KAAOA,UACPC,gBAAaC,MACdC,KAAOC,yBACTC,UAAUC,GAAG,SAAU,2BAA2B,SAASC,OACzDA,MAAMC,iBACNL,KAAKM,kBACNH,GAAG,QAAS,uBAAuB,WAClCH,KAAKO,cACNJ,GAAG,QAAS,yBAAyB,WACpCH,KAAKF,WAAaG,KAAKO,SAI/BC,eACId,QAAQN,KAAKqB,KAAM,4BAGvBC,cACIhB,QAAQN,KAAKqB,KAAM,uBAGvBE,UAAUC,SACFZ,KAAKJ,KAAKiB,SAASC,iBAGPd,KAAKJ,KAAKiB,SAASE,eAAeH,KAAKI,aAAcJ,KAAKK,QAEtEvB,QAAQN,KAAKqB,KAAM,uBAI3BS,mBACStB,KAAKiB,SAASM,eAAgB,OAC9BvB,KAAKiB,SAASC,WAAY,OAC1BlB,KAAKiB,SAASO,YACnBhC,KAAKiC,KAAK9B,SAAS+B,KACnB5B,QAAQN,KAAKqB,KAAM,uBAGvBc,kBACIC,OAAOC,SAAWA,SAASC,KAAKC,MAAM,KAAK,GAG/CC,SAAShB,SACDZ,KAAKJ,KAAKiB,SAASM,qBAGvB/B,KAAKqB,KAAKoB,KAAKjB,KAAKiB,MACpBzC,KAAK0C,KAAK1C,KAAKqB,YACTsB,QAAUnB,KAAKmB,YAChB,IAAIC,EAAI,EAAGA,EAAID,QAAQE,OAAQD,IAChC5C,KAAK8C,mBAAkB,mBAAE,IAAMH,QAAQC,GAAGG,YAAaJ,QAAQC,GAAGI,MACjC,UAA7BL,QAAQC,GAAGK,eACXjD,KAAKkD,qBAAqBP,QAAQC,GAAGI,KAAML,QAAQC,GAAGG,iBAGzDvC,KAAKiB,SAASM,eAAgB,EAGvCoB,YAAYC,UACR9C,QAAQH,SAASkD,MAAO,iCAAkC,WAAYD,UAM1EnC,kBACQL,KAAKJ,KAAKiB,SAAS6B,qBAIlB9C,KAAKiB,SAAS6B,UAAW,EACP,oBAAZC,SAEPA,QAAQC,oBAENC,WAAatD,SAASuD,KAAKC,qBAC7BnC,KAAO,OACN,IAAIoC,QAAQH,WACTA,WAAWI,eAAeD,QAC1BpC,KAAKiC,WAAWG,MAAMA,MAAQH,WAAWG,MAAMzC,OAGvDd,KAAKyD,KAAK,gBAAiBtC,MAAMA,OACzBA,KAAKuC,SAASlB,OAAS,EACvB7C,KAAK0C,KAAK1C,KAAKqB,KAAKoB,KAAKjB,KAAKuC,WAE9BzD,QAAQN,KAAKqB,KAAM,4BAElBb,KAAKiB,SAAS6B,UAAW,EACzB1C,KAAKJ,KAAKiB,SAASC,YAGpBd,KAAKJ,KAAKiB,SAASM,gBAGvB/B,KAAKiC,KAAK9B,SAAS+B,UACd1B,KAAKiB,SAASO,iBACpBgC,MAAK,UACCxD,KAAKiB,SAAS6B,UAAW,KAItCpC,WACIb,KAAKyD,KAAK,YAAa,CAACG,KAAMrD,KAAKH,aAAae,OACxB,YAAhBA,KAAK0C,OACL5D,QAAQN,KAAKqB,KAAM,uBAEnBf,QAAQN,KAAKqB,KAAM"}
\ No newline at end of file
+{"version":3,"file":"student.min.js","sources":["../src/student.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module mod_jazzquiz\n * @author Sebastian S. Gundersen \n * @copyright 2014 University of Wisconsin - Madison\n * @copyright 2018 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Quiz, Ajax, setText} from 'mod_jazzquiz/core';\nimport selectors from 'mod_jazzquiz/selectors';\n\nclass Student {\n\n /**\n * @param {Quiz} quiz\n */\n constructor(quiz) {\n this.quiz = quiz;\n this.voteAnswer = undefined;\n\n document.addEventListener('submit', event => {\n const questionForm = event.target.closest(selectors.question.form);\n if (questionForm) {\n event.preventDefault();\n this.submitAnswer();\n }\n });\n\n document.addEventListener('click', event => {\n const saveVoteButton = event.target.closest(selectors.quiz.saveVoteButton);\n if (saveVoteButton) {\n this.saveVote();\n }\n const selectVoteButton = event.target.closest(selectors.quiz.selectVoteButton);\n if (selectVoteButton) {\n this.voteAnswer = selectVoteButton.value;\n }\n });\n }\n\n onNotRunning() {\n setText(document.querySelector(selectors.quiz.info), 'instructions_for_student');\n }\n\n onPreparing() {\n setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');\n }\n\n onRunning(data) {\n if (this.quiz.question.isRunning) {\n return;\n }\n const started = this.quiz.question.startCountdown(data.questiontime, data.delay);\n if (!started) {\n setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');\n }\n }\n\n onReviewing() {\n this.quiz.question.isVoteRunning = false;\n this.quiz.question.isRunning = false;\n this.quiz.question.hideTimer();\n document.querySelector(selectors.question.box).classList.add('hidden');\n setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');\n }\n\n onSessionClosed() {\n window.location = location.href.split('&')[0];\n }\n\n onVoting(data) {\n if (this.quiz.question.isVoteRunning) {\n return;\n }\n const quizInfo = document.querySelector(selectors.quiz.info);\n quizInfo.innerHTML = data.html;\n quizInfo.classList.remove('hidden');\n const options = data.options;\n for (let i = 0; i < options.length; i++) {\n Quiz.addMathjaxElement(document.getElementById(options[i].content_id), options[i].text);\n if (options[i].question_type === 'stack') {\n Quiz.renderMaximaEquation(options[i].text, options[i].content_id);\n }\n }\n this.quiz.question.isVoteRunning = true;\n }\n\n onTimerTick(timeLeft) {\n setText(document.querySelector(selectors.question.timer), 'question_will_end_in_x_seconds', 'jazzquiz', timeLeft);\n }\n\n /**\n * Submit answer for the current question.\n */\n submitAnswer() {\n if (this.quiz.question.isSaving) {\n // Don't save twice.\n return;\n }\n this.quiz.question.isSaving = true;\n if (typeof tinyMCE !== 'undefined') {\n // eslint-disable-next-line no-undef\n tinyMCE.triggerSave();\n }\n const serialized = document.querySelector(selectors.question.form).serializeArray();\n let data = {};\n for (let name in serialized) {\n if (serialized.hasOwnProperty(name)) {\n data[serialized[name].name] = serialized[name].value;\n }\n }\n Ajax.post('save_question', data, data => {\n const quizInfo = document.querySelector(selectors.quiz.info);\n if (data.feedback.length > 0) {\n quizInfo.innerHTML = data.feedback;\n quizInfo.classList.remove('hidden');\n } else {\n setText(quizInfo, 'wait_for_instructor');\n }\n this.quiz.question.isSaving = false;\n if (!this.quiz.question.isRunning) {\n return;\n }\n if (this.quiz.question.isVoteRunning) {\n return;\n }\n document.querySelector(selectors.question.box).classList.add('hidden');\n this.quiz.question.hideTimer();\n }).fail(() => {\n this.quiz.question.isSaving = false;\n });\n }\n\n saveVote() {\n Ajax.post('save_vote', {vote: this.voteAnswer}, data => {\n const quizInfo = document.querySelector(selectors.quiz.info);\n if (data.status === 'success') {\n setText(quizInfo, 'wait_for_instructor');\n } else {\n setText(quizInfo, 'you_already_voted');\n }\n });\n }\n\n}\n\n/**\n * Initialize student session.\n */\nexport function initialize() {\n let quiz = new Quiz(Student);\n quiz.poll(2000);\n}\n"],"names":["Quiz","Student","poll","constructor","quiz","voteAnswer","undefined","document","addEventListener","event","target","closest","selectors","question","form","preventDefault","submitAnswer","saveVoteButton","saveVote","selectVoteButton","value","onNotRunning","querySelector","info","onPreparing","onRunning","data","this","isRunning","startCountdown","questiontime","delay","onReviewing","isVoteRunning","hideTimer","box","classList","add","onSessionClosed","window","location","href","split","onVoting","quizInfo","innerHTML","html","remove","options","i","length","addMathjaxElement","getElementById","content_id","text","question_type","renderMaximaEquation","onTimerTick","timeLeft","timer","isSaving","tinyMCE","triggerSave","serialized","serializeArray","name","hasOwnProperty","post","feedback","fail","vote","status"],"mappings":";;;;;;;4FAqKe,IAAIA,WAAKC,SACfC,KAAK,0EA5IRD,QAKFE,YAAYC,WACHA,KAAOA,UACPC,gBAAaC,EAElBC,SAASC,iBAAiB,UAAUC,QACXA,MAAMC,OAAOC,QAAQC,mBAAUC,SAASC,QAEzDL,MAAMM,sBACDC,mBAIbT,SAASC,iBAAiB,SAASC,QACRA,MAAMC,OAAOC,QAAQC,mBAAUR,KAAKa,sBAElDC,iBAEHC,iBAAmBV,MAAMC,OAAOC,QAAQC,mBAAUR,KAAKe,kBACzDA,wBACKd,WAAac,iBAAiBC,UAK/CC,iCACYd,SAASe,cAAcV,mBAAUR,KAAKmB,MAAO,4BAGzDC,gCACYjB,SAASe,cAAcV,mBAAUR,KAAKmB,MAAO,uBAGzDE,UAAUC,SACFC,KAAKvB,KAAKS,SAASe,iBAGPD,KAAKvB,KAAKS,SAASgB,eAAeH,KAAKI,aAAcJ,KAAKK,0BAE9DxB,SAASe,cAAcV,mBAAUR,KAAKmB,MAAO,uBAI7DS,mBACS5B,KAAKS,SAASoB,eAAgB,OAC9B7B,KAAKS,SAASe,WAAY,OAC1BxB,KAAKS,SAASqB,YACnB3B,SAASe,cAAcV,mBAAUC,SAASsB,KAAKC,UAAUC,IAAI,4BACrD9B,SAASe,cAAcV,mBAAUR,KAAKmB,MAAO,uBAGzDe,kBACIC,OAAOC,SAAWA,SAASC,KAAKC,MAAM,KAAK,GAG/CC,SAASjB,SACDC,KAAKvB,KAAKS,SAASoB,2BAGjBW,SAAWrC,SAASe,cAAcV,mBAAUR,KAAKmB,MACvDqB,SAASC,UAAYnB,KAAKoB,KAC1BF,SAASR,UAAUW,OAAO,gBACpBC,QAAUtB,KAAKsB,YAChB,IAAIC,EAAI,EAAGA,EAAID,QAAQE,OAAQD,eAC3BE,kBAAkB5C,SAAS6C,eAAeJ,QAAQC,GAAGI,YAAaL,QAAQC,GAAGK,MACjD,UAA7BN,QAAQC,GAAGM,0BACNC,qBAAqBR,QAAQC,GAAGK,KAAMN,QAAQC,GAAGI,iBAGzDjD,KAAKS,SAASoB,eAAgB,EAGvCwB,YAAYC,4BACAnD,SAASe,cAAcV,mBAAUC,SAAS8C,OAAQ,iCAAkC,WAAYD,UAM5G1C,kBACQW,KAAKvB,KAAKS,SAAS+C,qBAIlBxD,KAAKS,SAAS+C,UAAW,EACP,oBAAZC,SAEPA,QAAQC,oBAENC,WAAaxD,SAASe,cAAcV,mBAAUC,SAASC,MAAMkD,qBAC/DtC,KAAO,OACN,IAAIuC,QAAQF,WACTA,WAAWG,eAAeD,QAC1BvC,KAAKqC,WAAWE,MAAMA,MAAQF,WAAWE,MAAM7C,kBAGlD+C,KAAK,gBAAiBzC,MAAMA,aACvBkB,SAAWrC,SAASe,cAAcV,mBAAUR,KAAKmB,MACnDG,KAAK0C,SAASlB,OAAS,GACvBN,SAASC,UAAYnB,KAAK0C,SAC1BxB,SAASR,UAAUW,OAAO,6BAElBH,SAAU,4BAEjBxC,KAAKS,SAAS+C,UAAW,EACzBjC,KAAKvB,KAAKS,SAASe,YAGpBD,KAAKvB,KAAKS,SAASoB,gBAGvB1B,SAASe,cAAcV,mBAAUC,SAASsB,KAAKC,UAAUC,IAAI,eACxDjC,KAAKS,SAASqB,iBACpBmC,MAAK,UACCjE,KAAKS,SAAS+C,UAAW,KAItC1C,sBACSiD,KAAK,YAAa,CAACG,KAAM3C,KAAKtB,aAAaqB,aACtCkB,SAAWrC,SAASe,cAAcV,mBAAUR,KAAKmB,MACnC,YAAhBG,KAAK6C,yBACG3B,SAAU,yCAEVA,SAAU"}
\ No newline at end of file
diff --git a/amd/src/core.js b/amd/src/core.js
index 7bab53f..20a5a1f 100755
--- a/amd/src/core.js
+++ b/amd/src/core.js
@@ -22,76 +22,21 @@
*/
import $ from 'jquery';
-import mString from 'core/str';
-import mEvent from 'core_filters/events';
+import {get_string} from 'core/str';
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
import selectors from 'mod_jazzquiz/selectors';
-import Question from "mod_jazzquiz/question";
+import {Question} from 'mod_jazzquiz/question';
// Contains the needed values for using the ajax script.
let session = {
courseModuleId: 0,
- activityId: 0, // TODO: Remove activityId? Unsure if used.
sessionId: 0,
attemptId: 0,
sessionKey: ''
};
-// Used for caching the latex of maxima input.
-let cache = [];
-
-// TODO: Migrate to core/ajax module?
-class Ajax {
-
- /**
- * Send a request using AJAX, with method specified.
- *
- * @param {string} method Which HTTP method to use.
- * @param {string} url Relative to root of jazzquiz module. Does not start with /.
- * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey
- * @param {function} success Callback function for when the request was completed successfully.
- * @return {jqXHR} The jQuery XHR object
- */
- static request(method, url, data, success) {
- data.id = session.courseModuleId;
- data.sessionid = session.sessionId;
- data.attemptid = session.attemptId;
- data.sesskey = session.sessionKey;
- return $.ajax({
- type: method,
- url: url,
- data: data,
- dataType: 'json',
- success: success
- }).fail(() => setText(Quiz.info, 'error_with_request'));
- }
-
- /**
- * Send a GET request using AJAX.
- * @param {string} action Which action to query.
- * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey
- * @param {function} success Callback function for when the request was completed successfully.
- * @return {jqXHR} The jQuery XHR object
- */
- static get(action, data, success) {
- data.action = action;
- return Ajax.request('get', 'ajax.php', data, success);
- }
-
- /**
- * Send a POST request using AJAX.
- * @param {string} action Which action to query.
- * @param {Object} data Object with parameters as properties. Reserved: id, quizid, sessionid, attemptid, sesskey
- * @param {function} success Callback function for when the request was completed successfully.
- * @return {jqXHR} The jQuery XHR object
- */
- static post(action, data, success) {
- data.action = action;
- return Ajax.request('post', 'ajax.php', data, success);
- }
-
-}
-
-class Quiz {
+export class Quiz {
constructor(Role) {
this.state = '';
@@ -108,129 +53,58 @@ class Quiz {
};
}
- changeQuizState(state, data) {
- this.isNewState = (this.state !== state);
- this.state = state;
+ changeQuizState(data) {
+ this.isNewState = (this.state !== data.state);
+ this.state = data.state;
if (this.role.onStateChange !== undefined) {
this.role.onStateChange();
}
- const event = this.events[state];
+ const event = this.events[data.state];
this.role[event](data);
}
/**
- * Initiate the chained session info calls to ajax.php
+ * Start polling for the current session state.
* @param {number} ms interval in milliseconds
*/
poll(ms) {
- Ajax.get('info', {}, data => {
- this.changeQuizState(data.status, data);
+ Ajax.call({
+ methodname: 'mod_jazzquiz_poll',
+ args: {cmid: session.courseModuleId, sessionid: session.sessionId}
+ })[0].done(data => {
+ this.changeQuizState(data);
setTimeout(() => this.poll(ms), ms);
- });
- }
-
- static get main() {
- return document.querySelector(selectors.main);
- }
-
- static get info() {
- return document.querySelector(selectors.quiz.info);
- }
-
- static get responded() {
- return document.querySelector(selectors.quiz.responded);
- }
-
- static get responses() {
- return document.querySelector(selectors.quiz.responses);
- }
-
- static get responseInfo() {
- return document.querySelector(selectors.quiz.responseInfo);
- }
-
- static hide($element) {
- $element.addClass('hidden');
- }
-
- static show($element) {
- $element.removeClass('hidden');
- }
-
- static uncheck($element) {
- $element.children('.fa').removeClass('fa-check-square-o').addClass('fa-square-o');
- }
-
- static check($element) {
- $element.children('.fa').removeClass('fa-square-o').addClass('fa-check-square-o');
- }
-
- /**
- * Triggers a dynamic content update event, which MathJax listens to.
- */
- static renderAllMathjax() {
- mEvent.notifyFilterContentUpdated(document.getElementsByClassName('jazzquiz-response-container'));
- }
-
- /**
- * Sets the body of the target, and triggers an event letting MathJax know about the element.
- * @param {*} $target
- * @param {string} latex
- */
- static addMathjaxElement($target, latex) {
- $target.html('' + latex + ' ');
- Quiz.renderAllMathjax();
- }
-
- /**
- * Converts the input to LaTeX and renders it to the target with MathJax.
- * @param {string} input
- * @param {string} targetId
- */
- static renderMaximaEquation(input, targetId) {
- const target = document.getElementById(targetId);
- if (target === null) {
- // Log error to console: 'Target element #' + targetId + ' not found.'.
- return;
- }
- if (cache[input] !== undefined) {
- Quiz.addMathjaxElement($('#' + targetId), cache[input]);
- return;
- }
- Ajax.get('stack', {input: encodeURIComponent(input)}, data => {
- cache[data.original] = data.latex;
- Quiz.addMathjaxElement($('#' + targetId), data.latex);
- });
+ }).fail(Notification.exception);
}
}
/**
* Retrieve a language string that was sent along with the page.
- * @param {*} $element
+ * @param {*} element
* @param {string} key Which string in the language file we want.
* @param {string} [from=jazzquiz] Which language file we want the string from. Default is jazzquiz.
* @param {array} [args=[]] This is {$a} in the string for the key.
*/
-export function setText($element, key, from, args) {
+export function setText(element, key, from, args) {
from = (from !== undefined) ? from : 'jazzquiz';
args = (args !== undefined) ? args : [];
- $.when(mString.get_string(key, from, args))
- .done(text => Quiz.show($element.html(text)));
+ $.when(get_string(key, from, args)).done(text => {
+ element.innerHTML = text;
+ element.classList.remove('hidden');
+ });
}
/**
* Initialize session data.
*
* @param {number} courseModuleId
- * @param {number} activityId
* @param {number} sessionId
* @param {number} attemptId
* @param {string} sessionKey
*/
-export function initialize(courseModuleId, activityId, sessionId, attemptId, sessionKey) {
+export function initialize(courseModuleId, sessionId, attemptId, sessionKey) {
session.courseModuleId = courseModuleId;
- session.activityId = activityId;
session.sessionId = sessionId;
session.attemptId = attemptId;
session.sessionKey = sessionKey;
diff --git a/amd/src/instructor.js b/amd/src/instructor.js
index 0b2222d..2a5fdcc 100755
--- a/amd/src/instructor.js
+++ b/amd/src/instructor.js
@@ -22,13 +22,10 @@
*/
import $ from 'jquery';
-import Jazz from 'mod_jazzquiz/core';
+import {Quiz, setText} from 'mod_jazzquiz/core';
+import {Question} from 'mod_jazzquiz/question';
import selectors from 'mod_jazzquiz/selectors';
-
-const Quiz = Jazz.Quiz;
-const Question = Jazz.Question;
-const Ajax = Jazz.Ajax;
-const setText = Jazz.setText;
+import {addMathjaxElement, renderMaximaEquation, renderAllMathjax} from 'mod_jazzquiz/math_rendering';
class ResponseView {
@@ -72,26 +69,26 @@ class ResponseView {
* Clear, but not hide responses.
*/
clear() {
- Quiz.responses.html('');
- Quiz.responseInfo.html('');
+ document.querySelector(selectors.quiz.responses).innerHTML = '';
+ document.querySelector(selectors.quiz.responseInfo).innerHTML = '';
}
/**
* Hides the responses
*/
hide() {
- Quiz.uncheck(Instructor.control('responses'));
- Quiz.hide(Quiz.responses);
- Quiz.hide(Quiz.responseInfo);
+ Instructor.control('responses').querySelector('.fa').classList.replace('fa-check-square-o', 'fa-square-o');
+ document.querySelector(selectors.quiz.responses).classList.add('hidden');
+ document.querySelector(selectors.quiz.responseInfo).classList.add('hidden');
}
/**
* Shows the responses
*/
show() {
- Quiz.check(Instructor.control('responses'));
- Quiz.show(Quiz.responses);
- Quiz.show(Quiz.responseInfo);
+ Instructor.control('responses').querySelector('.fa').classList.replace('fa-square-o', 'fa-check-square-o');
+ document.querySelector(selectors.quiz.responses).classList.remove('hidden');
+ document.querySelector(selectors.quiz.responseInfo).classList.remove('hidden');
if (this.showVotesUponReview) {
this.refreshVotes();
this.showVotesUponReview = false;
@@ -167,28 +164,32 @@ class ResponseView {
* @param {string} name Can be either 'vote_response' or 'current_response'
*/
createControls(name) {
+ const quizResponseInfo = document.querySelector(selectors.quiz.responseInfo);
if (!this.quiz.question.hasVotes) {
- Quiz.hide(Quiz.responseInfo);
+ quizResponseInfo.classList.add('hidden');
return;
}
// Add button for instructor to change what to review.
if (this.quiz.state === 'reviewing') {
- let $showNormalResult = $('#review_show_normal_results');
- let $showVoteResult = $('#review_show_vote_results');
- Quiz.show(Quiz.responseInfo);
+ let showNormalResult = document.querySelector(selectors.quiz.showNormalResultsButton);
+ let showVoteResult = document.querySelector(selectors.quiz.showVoteResultsButton);
+ quizResponseInfo.classList.remove('hidden');
if (name === 'vote_response') {
- if ($showNormalResult.length === 0) {
- setText(Quiz.responseInfo.html(' ').children('h4'), 'showing_vote_results');
- Quiz.responseInfo.append(' ');
- setText($('#review_show_normal_results'), 'click_to_show_original_results');
- $showVoteResult.remove();
+ if (!showNormalResult) {
+ setText(quizResponseInfo.html(' ').children('h4'), 'showing_vote_results');
+ quizResponseInfo.innerHTML += ' ';
+ showNormalResult = document.querySelector(selectors.quiz.showNormalResultsButton);
+ setText(showNormalResult, 'click_to_show_original_results');
+ showVoteResult.remove();
}
} else if (name === 'current_response') {
- if ($showVoteResult.length === 0) {
- setText(Quiz.responseInfo.html(' ').children('h4'), 'showing_original_results');
- Quiz.responseInfo.append(' ');
- setText($('#review_show_vote_results'), 'click_to_show_vote_results');
- $showNormalResult.remove();
+ if (!showVoteResult) {
+ quizResponseInfo.innerHTML = ' ';
+ setText(quizResponseInfo.querySelector('h4'), 'showing_original_results');
+ quizResponseInfo.innerHTML += ' ';
+ showVoteResult = document.querySelector(selectors.quiz.showVoteResultsButton);
+ setText(showVoteResult, 'click_to_show_vote_results');
+ showNormalResult.remove();
}
}
}
@@ -224,9 +225,7 @@ class ResponseView {
const countHtml = '' + response.count + ' ';
let responseCell = row.insertCell(0);
- responseCell.onclick = function() {
- $(this).parent().toggleClass('selected-vote-option');
- };
+ responseCell.onclick = () => responseCell.parentElement.classList.toggle('selected-vote-option');
let barCell = row.insertCell(1);
barCell.classList.add('bar');
@@ -235,9 +234,9 @@ class ResponseView {
const latexId = name + '_latex_' + rowIndex;
responseCell.innerHTML = ' ';
- Quiz.addMathjaxElement($('#' + latexId), response.response);
+ addMathjaxElement(document.getElementById(latexId), response.response);
if (response.qtype === 'stack') {
- Quiz.renderMaximaEquation(response.response, latexId);
+ renderMaximaEquation(response.response, latexId);
}
} else {
let currentRow = target.rows[currentRowIndex];
@@ -350,11 +349,12 @@ class ResponseView {
if (responses === undefined) {
return;
}
+ const quizResponded = document.querySelector(selectors.quiz.responded);
// Check if any responses to show.
if (responses.length === 0) {
- Quiz.show(Quiz.responded);
- setText(Quiz.responded.find('h4'), 'a_out_of_b_responded', 'jazzquiz', {
+ quizResponded.classList.remove('hidden');
+ setText(quizResponded.querySelector('h4'), 'a_out_of_b_responded', 'jazzquiz', {
a: 0,
b: this.totalStudents
});
@@ -407,9 +407,9 @@ class ResponseView {
}
// Update responded container.
- if (Quiz.responded.length !== 0 && responded !== undefined) {
- Quiz.show(Quiz.responded);
- setText(Quiz.responded.find('h4'), 'a_out_of_b_responded', 'jazzquiz', {
+ if (quizResponded.length !== 0 && responded !== undefined) {
+ quizResponded.classList.remove('hidden');
+ setText(quizResponded.querySelector('h4'), 'a_out_of_b_responded', 'jazzquiz', {
a: responded,
b: this.totalStudents
});
@@ -417,14 +417,15 @@ class ResponseView {
// Should we show the responses?
if (!this.showResponses && this.quiz.state !== 'reviewing') {
- Quiz.hide(Quiz.responseInfo);
- Quiz.hide(Quiz.responses);
+ document.querySelector(selectors.quiz.responseInfo).classList.add('hidden');
+ document.querySelector(selectors.quiz.responses).classList.add('hidden');
return;
}
if (document.getElementById(tableId) === null) {
- const html = '';
- Quiz.show($('#' + wrapperId).html(html));
+ const wrapper = document.getElementById(wrapperId);
+ wrapper.innerHTML = ``;
+ wrapper.classList.remove('hidden');
}
this.createBarGraph(this.currentResponses, 'current_response', tableId, graphId, rebuild);
ResponseView.sortBarGraph(tableId);
@@ -442,10 +443,9 @@ class ResponseView {
this.set('jazzquiz_responses_container', 'current_responses_wrapper',
data.responses, data.responded, data.question_type, 'results', rebuild);
- if (data.merge_count > 0) {
- Quiz.show($('#jazzquiz_undo_merge'));
- } else {
- Quiz.hide($('#jazzquiz_undo_merge'));
+ const undoMergeButton = document.querySelector(selectors.quiz.undoMergeButton);
+ if (undoMergeButton) {
+ undoMergeButton.classList.toggle('hidden', data.merge_count <= 0);
}
});
}
@@ -454,20 +454,20 @@ class ResponseView {
* Method refresh() equivalent for votes.
*/
refreshVotes() {
+ const quizResponses = document.querySelector(selectors.quiz.responses);
+ const quizResponseInfo = document.querySelector(selectors.quiz.responseInfo);
// Should we show the results?
if (!this.showResponses && this.quiz.state !== 'reviewing') {
- Quiz.hide(Quiz.responseInfo);
- Quiz.hide(Quiz.responses);
+ quizResponseInfo.classList.add('hidden');
+ quizResponses.classList.add('hidden');
return;
}
Ajax.get('get_vote_results', {}, data => {
const answers = data.answers;
const targetId = 'wrapper_vote_responses';
let responses = [];
-
this.respondedCount = 0;
this.totalStudents = parseInt(data.total_students);
-
for (let i in answers) {
if (!answers.hasOwnProperty(i)) {
continue;
@@ -480,17 +480,14 @@ class ResponseView {
});
this.respondedCount += parseInt(answers[i].finalcount);
}
-
- setText(Quiz.responded.find('h4'), 'a_out_of_b_voted', 'jazzquiz', {
+ setText(document.querySelector(selectors.quiz.responded + ' h4'), 'a_out_of_b_voted', 'jazzquiz', {
a: this.respondedCount,
b: this.totalStudents
});
-
if (document.getElementById(targetId) === null) {
- const html = '';
- Quiz.show(Quiz.responses.html(html));
+ quizResponses.innerHTML = ``;
+ quizResponses.classList.remove('hidden');
}
-
this.createBarGraph(responses, 'vote_response', targetId, 'vote', false);
ResponseView.sortBarGraph(targetId);
});
@@ -510,17 +507,12 @@ class Instructor {
this.totalQuestions = 0;
this.allowVote = false;
- $(document).on('keyup', event => {
- if (event.keyCode === 27) { // Escape key.
+ document.addEventListener('keyup', event => {
+ if (event.key === 'Escape') {
Instructor.closeFullscreenView();
}
});
- $(document).on('click', event => {
- Instructor.closeQuestionListMenu(event, 'improvise');
- Instructor.closeQuestionListMenu(event, 'jump');
- });
-
Instructor.addEvents({
'repoll': () => this.repollQuestion(),
'vote': () => this.runVoting(),
@@ -549,6 +541,17 @@ class Instructor {
'm': 'random',
'f': 'fullscreen'
});
+
+ document.addEventListener('click', event => {
+ Instructor.closeQuestionListMenu(event, 'improvise');
+ Instructor.closeQuestionListMenu(event, 'jump');
+ if (event.target.closest(selectors.quiz.startQuizButton)) {
+ this.startQuiz();
+ }
+ if (event.target.closest(selectors.quiz.exitQuizButton)) {
+ this.closeSession();
+ }
+ });
}
static addHotkeys(keys) {
@@ -558,11 +561,12 @@ class Instructor {
action: keys[key],
repeat: false // TODO: Maybe event.repeat becomes more standard?
};
- $(document).on('keydown', event => {
+
+ document.addEventListener('keydown', event => {
if (keys[key].repeat || event.ctrlKey) {
return;
}
- if (String.fromCharCode(event.which).toLowerCase() !== key) {
+ if (event.key.toLowerCase() !== key) {
return;
}
let focusedTag = $(':focus').prop('tagName');
@@ -579,8 +583,9 @@ class Instructor {
$control.click();
}
});
- $(document).on('keyup', event => {
- if (String.fromCharCode(event.which).toLowerCase() === key) {
+
+ document.addEventListener('keyup', event => {
+ if (event.key.toLowerCase() === key) {
keys[key].repeat = false;
}
});
@@ -589,34 +594,33 @@ class Instructor {
}
static addEvents(events) {
- for (let key in events) {
- if (events.hasOwnProperty(key)) {
- $(document).on('click', '#jazzquiz_control_' + key, () => {
- Instructor.enableControls([]);
- events[key]();
- });
+ document.addEventListener('click', event => {
+ const controlButton = event.target.closest(selectors.quiz.controlButton);
+ if (controlButton) {
+ Instructor.enableControls([]);
+ events[controlButton.dataset.control]();
}
- }
+ });
}
static get controls() {
- return $('#jazzquiz_controls_box');
+ return document.querySelector(selectors.quiz.controlsBox);
}
static get controlButtons() {
- return Instructor.controls.find('.quiz-control-buttons');
+ return document.querySelector(selectors.quiz.controlButtons);
}
static control(key) {
- return $('#jazzquiz_control_' + key);
+ return document.getElementById(`jazzquiz_control_${key}`);
}
static get side() {
- return $('#jazzquiz_side_container');
+ return document.querySelector(selectors.quiz.sideContainer);
}
static get correctAnswer() {
- return $('#jazzquiz_correct_answer_container');
+ return document.querySelector(selectors.quiz.correctAnswerContainer);
}
static get isMerging() {
@@ -625,24 +629,25 @@ class Instructor {
onNotRunning(data) {
this.responses.totalStudents = data.student_count;
- Quiz.hide(Instructor.side);
- setText(Quiz.info, 'instructions_for_instructor');
+ Instructor.side.classList.add('hidden');
+ setText(document.querySelector(selectors.quiz.info), 'instructions_for_instructor');
Instructor.enableControls([]);
- Quiz.hide(Instructor.controlButtons);
- let $studentsJoined = Instructor.control('startquiz').next();
+ document.querySelector(selectors.quiz.controlButtons).classList.add('hidden');
+ const studentsJoined = document.querySelector(selectors.quiz.studentsJoinedCounter);
if (data.student_count === 1) {
- setText($studentsJoined, 'one_student_has_joined');
+ setText(studentsJoined, 'one_student_has_joined');
} else if (data.student_count > 1) {
- setText($studentsJoined, 'x_students_have_joined', 'jazzquiz', data.student_count);
+ setText(studentsJoined, 'x_students_have_joined', 'jazzquiz', data.student_count);
} else {
- setText($studentsJoined, 'no_students_have_joined');
+ setText(studentsJoined, 'no_students_have_joined');
}
- Quiz.show(Instructor.control('startquiz').parent());
+ const startQuizButton = Instructor.control('startquiz');
+ startQuizButton.parentElement.classList.remove('hidden');
}
onPreparing(data) {
- Quiz.hide(Instructor.side);
- setText(Quiz.info, 'instructions_for_instructor');
+ Instructor.side.classList.add('hidden');
+ setText(document.querySelector(selectors.quiz.info), 'instructions_for_instructor');
let enabledButtons = ['improvise', 'jump', 'random', 'fullscreen', 'quit'];
if (data.slot < this.totalQuestions) {
enabledButtons.push('next');
@@ -654,7 +659,7 @@ class Instructor {
if (!this.responses.showResponses) {
this.responses.hide();
}
- Quiz.show(Instructor.side);
+ Instructor.side.classList.remove('hidden');
Instructor.enableControls(['end', 'responses', 'fullscreen']);
this.quiz.question.questionTime = data.questiontime;
if (this.quiz.question.isRunning) {
@@ -674,7 +679,7 @@ class Instructor {
}
onReviewing(data) {
- Quiz.show(Instructor.side);
+ Instructor.side.classList.remove('hidden');
let enabledButtons = ['answer', 'repoll', 'fullscreen', 'improvise', 'jump', 'random', 'quit'];
if (this.allowVote) {
enabledButtons.push('vote');
@@ -699,8 +704,8 @@ class Instructor {
}
onSessionClosed() {
- Quiz.hide(Instructor.side);
- Quiz.hide(Instructor.correctAnswer);
+ Instructor.side.classList.add('hidden');
+ Instructor.correctAnswer.classList.add('hidden');
Instructor.enableControls([]);
this.responses.clear();
this.quiz.question.isRunning = false;
@@ -710,17 +715,19 @@ class Instructor {
if (!this.responses.showResponses) {
this.responses.hide();
}
- Quiz.show(Instructor.side);
+ Instructor.side.classList.remove('hidden');
Instructor.enableControls(['quit', 'fullscreen', 'answer', 'responses', 'end']);
this.responses.refreshVotes();
}
onStateChange() {
- $('#region-main').find('ul.nav.nav-tabs').css('display', 'none');
- $('#region-main-settings-menu').css('display', 'none');
- $('.region_main_settings_menu_proxy').css('display', 'none');
- Quiz.show(Instructor.controlButtons);
- Quiz.hide(Instructor.control('startquiz').parent());
+ //$('#region-main').find('ul.nav.nav-tabs').css('display', 'none');
+ //$('#region-main-settings-menu').css('display', 'none');
+ //$('.region_main_settings_menu_proxy').css('display', 'none');
+
+ Instructor.controlButtons.classList.remove('hidden');
+ Instructor.control('startquiz').parentElement.classList.add('hidden');
+
}
onQuestionRefreshed(data) {
@@ -732,15 +739,20 @@ class Instructor {
}
onTimerTick(timeLeft) {
- setText(Question.timer, 'x_seconds_left', 'jazzquiz', timeLeft);
+ setText(document.querySelector(selectors.question.timer), 'x_seconds_left', 'jazzquiz', timeLeft);
}
/**
* Start the quiz. Does not start any questions.
*/
startQuiz() {
- Quiz.hide(Instructor.control('startquiz').parent());
- Ajax.post('start_quiz', {}, () => $('#jazzquiz_controls').removeClass('btn-hide'));
+ Instructor.control('startquiz').parentElement.classList.add('hidden');
+ Ajax.post('start_quiz', {}, () => {
+ const controls = document.querySelector(selectors.quiz.controls);
+ if (controls) {
+ controls.classList.remove('btn-hide');
+ }
+ });
}
/**
@@ -763,39 +775,39 @@ class Instructor {
* @param {string} name
*/
showQuestionListSetup(name) {
- let $controlButton = Instructor.control(name);
- if ($controlButton.hasClass('active')) {
+ let controlButton = Instructor.control(name);
+ if (controlButton.classList.contains('active')) {
// It's already open. Let's not send another request.
return;
}
- Ajax.get('list_' + name + '_questions', {}, data => {
- let self = this;
- let $menu = $('#jazzquiz_' + name + '_menu');
- const menuMargin = $controlButton.offset().left - $controlButton.parent().offset().left;
- $menu.html('').addClass('active').css('margin-left', menuMargin + 'px');
- $controlButton.addClass('active');
+ Ajax.get(`list_${name}_questions`, {}, data => {
+ controlButton.classList.add('active');
+ const menu = document.querySelector(`#jazzquiz_${name}_menu`);
+ menu.innerHTML = '';
+ menu.classList.add('active');
+ const margin = controlButton.getBoundingClientRect().left - controlButton.parentElement.getBoundingClientRect().left;
+ menu.style.marginLeft = margin + 'px';
const questions = data.questions;
for (let i in questions) {
if (!questions.hasOwnProperty(i)) {
continue;
}
- let $questionButton = $(' ');
- Quiz.addMathjaxElement($questionButton, questions[i].name);
- $questionButton.data({
- 'time': questions[i].time,
- 'question-id': questions[i].questionid,
- 'jazzquiz-question-id': questions[i].jazzquizquestionid
- });
- $questionButton.data('test', 1);
- $questionButton.on('click', function() {
- const questionId = $(this).data('question-id');
- const time = $(this).data('time');
- const jazzQuestionId = $(this).data('jazzquiz-question-id');
- self.jumpQuestion(questionId, time, jazzQuestionId);
- $menu.html('').removeClass('active');
- $controlButton.removeClass('active');
+ let questionButton = document.createElement('BUTTON');
+ questionButton.classList.add('btn');
+ addMathjaxElement(questionButton, questions[i].name);
+ questionButton.dataset.time = questions[i].time;
+ questionButton.dataset.questionId = questions[i].questionid;
+ questionButton.dataset.jazzquizQuestionId = questions[i].jazzquizquestionid;
+ questionButton.addEventListener('click', () => {
+ const questionId = questionButton.dataset.questionId;
+ const time = questionButton.dataset.time;
+ const jazzQuestionId = questionButton.dataset.jazzquizQuestionId;
+ this.jumpQuestion(questionId, time, jazzQuestionId);
+ menu.innerHTML = '';
+ menu.classList.remove('active');
+ controlButton.classList.remove('active');
});
- $menu.append($questionButton);
+ menu.appendChild(questionButton);
}
});
}
@@ -805,14 +817,9 @@ class Instructor {
* @returns {Array.} Vote options
*/
static getSelectedAnswersForVote() {
- let result = [];
- $('.selected-vote-option').each((i, option) => {
- result.push({
- text: option.dataset.response,
- count: option.dataset.count
- });
+ return document.querySelectorAll(selectors.quiz.selectedVoteOption).map(option => {
+ return {text: option.dataset.response, count: option.dataset.count};
});
- return result;
}
/**
@@ -832,7 +839,7 @@ class Instructor {
* @param {number} jazzquizQuestionId
*/
startQuestion(method, questionId, questionTime, jazzquizQuestionId) {
- Quiz.hide(Quiz.info);
+ document.querySelector(selectors.quiz.info).classList.add('hidden');
this.responses.clear();
this.hideCorrectAnswer();
Ajax.post('start_question', {
@@ -878,10 +885,10 @@ class Instructor {
* Close the current session.
*/
closeSession() {
- Quiz.hide($('#jazzquiz_undo_merge'));
- Quiz.hide(Question.box);
- Quiz.hide(Instructor.controls);
- setText(Quiz.info, 'closing_session');
+ document.querySelector(selectors.quiz.undoMergeButton).classList.add('hidden');
+ document.querySelector(selectors.question.box).classList.add('hidden');
+ Instructor.controls.classList.add('hidden');
+ setText(document.querySelector(selectors.quiz.info), 'closing_session');
// eslint-disable-next-line no-return-assign
Ajax.post('close_session', {}, () => window.location = location.href.split('&')[0]);
}
@@ -891,8 +898,8 @@ class Instructor {
*/
hideCorrectAnswer() {
if (this.isShowingCorrectAnswer) {
- Quiz.hide(Instructor.correctAnswer);
- Quiz.uncheck(Instructor.control('answer'));
+ Instructor.correctAnswer.classList.add('hidden');
+ Instructor.control('answer').querySelector('.fa').classList.replace('fa-check-square-o', 'fa-square-o');
this.isShowingCorrectAnswer = false;
}
}
@@ -903,9 +910,10 @@ class Instructor {
showCorrectAnswer() {
this.hideCorrectAnswer();
Ajax.get('get_right_response', {}, data => {
- Quiz.show(Instructor.correctAnswer.html(data.right_answer));
- Quiz.renderAllMathjax();
- Quiz.check(Instructor.control('answer'));
+ Instructor.correctAnswer.innerHTML = data.right_answer;
+ Instructor.correctAnswer.classList.remove('hidden');
+ renderAllMathjax();
+ Instructor.control('answer').querySelector('.fa').classList.replace('fa-square-o', 'fa-check-square-o');
this.isShowingCorrectAnswer = true;
});
}
@@ -915,9 +923,8 @@ class Instructor {
* @param {Array.} buttons The unique part of the IDs of the buttons to be enabled.
*/
static enableControls(buttons) {
- Instructor.controlButtons.children('button').each((index, child) => {
- const id = child.getAttribute('id').replace('jazzquiz_control_', '');
- child.disabled = (buttons.indexOf(id) === -1);
+ document.querySelectorAll(selectors.quiz.controlButton).forEach(controlButton => {
+ controlButton.disabled = (buttons.indexOf(controlButton.dataset.control) === -1);
});
}
@@ -925,14 +932,14 @@ class Instructor {
* Enter fullscreen mode for better use with projectors.
*/
static showFullscreenView() {
- if (Quiz.main.hasClass('jazzquiz-fullscreen')) {
+ if (document.querySelector(selectors.main).classList.contains('jazzquiz-fullscreen')) {
Instructor.closeFullscreenView();
return;
}
// Hide the scrollbar - remember to always set back to auto when closing.
document.documentElement.style.overflowY = 'hidden';
// Sets the quiz view to an absolute position that covers the viewport.
- Quiz.main.addClass('jazzquiz-fullscreen');
+ document.querySelector(selectors.main).classList.add('jazzquiz-fullscreen');
}
/**
@@ -940,7 +947,7 @@ class Instructor {
*/
static closeFullscreenView() {
document.documentElement.style.overflowY = 'auto';
- Quiz.main.removeClass('jazzquiz-fullscreen');
+ document.querySelector(selectors.main).classList.remove('jazzquiz-fullscreen');
}
/**
@@ -949,24 +956,30 @@ class Instructor {
* @param {string} name
*/
static closeQuestionListMenu(event, name) {
- const menuId = '#jazzquiz_' + name + '_menu';
// Close the menu if the click was not inside.
- const menu = $(event.target).closest(menuId);
- if (!menu.length) {
- $(menuId).html('').removeClass('active');
- Instructor.control(name).removeClass('active');
+ if (!event.target.closest(`#jazzquiz_${name}_menu`)) {
+ const menu = document.querySelector(`#jazzquiz_${name}_menu`);
+ menu.innerHTML = '';
+ menu.classList.remove('active');
+ const controlButton = document.querySelector(`#jazzquiz_control_${name}`);
+ if (controlButton) {
+ controlButton.classList.remove('active');
+ }
}
}
static addReportEventHandlers() {
- $(document).on('click', '#report_overview_controls button', function() {
- const action = $(this).data('action');
- if (action === 'attendance') {
- $('#report_overview_responded').fadeIn();
- $('#report_overview_responses').fadeOut();
- } else if (action === 'responses') {
- $('#report_overview_responses').fadeIn();
- $('#report_overview_responded').fadeOut();
+ document.addEventListener('click', event => {
+ const reportOverviewControlsButton = event.target.closest('#report_overview_controls button');
+ if (reportOverviewControlsButton) {
+ const action = reportOverviewControlsButton.dataset.action;
+ if (action === 'attendance') {
+ document.querySelector(selectors.quiz.reportOverviewResponded).classList.remove('hidden');
+ document.querySelector(selectors.quiz.reportOverviewResponses).classList.add('hidden');
+ } else if (action === 'responses') {
+ document.querySelector(selectors.quiz.reportOverviewResponses).classList.remove('hidden');
+ document.querySelector(selectors.quiz.reportOverviewResponded).classList.add('hidden');
+ }
}
});
}
@@ -974,7 +987,7 @@ class Instructor {
}
export const initialize = (totalQuestions, reportView, slots) => {
- let quiz = new Quiz(Instructor);
+ const quiz = new Quiz(Instructor);
quiz.role.totalQuestions = totalQuestions;
if (reportView) {
Instructor.addReportEventHandlers();
diff --git a/amd/src/math_rendering.js b/amd/src/math_rendering.js
new file mode 100644
index 0000000..2906a6c
--- /dev/null
+++ b/amd/src/math_rendering.js
@@ -0,0 +1,65 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * @module mod_jazzquiz
+ * @author Sebastian S. Gundersen
+ * @copyright 2024 NTNU
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {notifyFilterContentUpdated} from 'core_filters/events';
+import Ajax from 'core/ajax';
+import selectors from 'mod_jazzquiz/selectors';
+
+// Used for caching the latex of maxima input.
+let cache = [];
+
+/**
+ * Triggers a dynamic content update event, which MathJax listens to.
+ */
+export function renderAllMathjax() {
+ notifyFilterContentUpdated(document.querySelectorAll(selectors.quiz.responseContainer));
+}
+
+/**
+ * Sets the body of the target, and triggers an event letting MathJax know about the element.
+ * @param {HTMLElement} target
+ * @param {string} latex
+ */
+export function addMathjaxElement(target, latex) {
+ target.innerHTML = `${latex} `;
+ renderAllMathjax();
+}
+
+/**
+ * Converts the input to LaTeX and renders it to the target with MathJax.
+ * @param {string} input
+ * @param {string} targetId
+ */
+export function renderMaximaEquation(input, targetId) {
+ const target = document.getElementById(targetId);
+ if (target === null) {
+ return;
+ }
+ if (cache[input] !== undefined) {
+ addMathjaxElement(document.getElementById(targetId), cache[input]);
+ return;
+ }
+ Ajax.get('stack', {input: encodeURIComponent(input)}, data => {
+ cache[data.original] = data.latex;
+ addMathjaxElement(document.getElementById(targetId), data.latex);
+ });
+}
diff --git a/amd/src/question.js b/amd/src/question.js
index 8890ee0..b18a9a8 100644
--- a/amd/src/question.js
+++ b/amd/src/question.js
@@ -13,8 +13,9 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-import {Quiz, Ajax, setText} from 'mod_jazzquiz/core';
+import {Ajax, setText} from 'mod_jazzquiz/core';
import selectors from 'mod_jazzquiz/selectors';
+import {renderAllMathjax} from 'mod_jazzquiz/math_rendering';
/**
* Current question.
@@ -40,28 +41,18 @@ export class Question {
this.timerInterval = 0;
}
- static get box() {
- return document.querySelector(selectors.question.box);
- }
-
- static get timer() {
- return document.querySelector(selectors.question.timer);
- }
-
- static get form() {
- return document.querySelector(selectors.question.form);
- }
-
/**
* Request the current question form.
*/
refresh() {
Ajax.get('get_question_form', {}, data => {
if (data.is_already_submitted) {
- setText(Quiz.info, 'wait_for_instructor');
+ setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');
return;
}
- Quiz.show(Question.box.html(data.html));
+ const questionBox = document.querySelector(selectors.question.box);
+ questionBox.innerHTML = data.html;
+ questionBox.classList.remove('hidden');
// eslint-disable-next-line no-eval
eval(data.js);
data.css.forEach(cssUrl => {
@@ -75,7 +66,7 @@ export class Question {
if (this.quiz.role.onQuestionRefreshed !== undefined) {
this.quiz.role.onQuestionRefreshed(data);
}
- Quiz.renderAllMathjax();
+ renderAllMathjax();
});
}
@@ -83,7 +74,7 @@ export class Question {
* Hide the question "ending in" timer, and clears the interval.
*/
hideTimer() {
- Quiz.hide(Question.timer);
+ document.querySelector(selectors.question.timer).classList.add('hidden');
clearInterval(this.timerInterval);
this.timerInterval = 0;
}
@@ -93,15 +84,16 @@ export class Question {
* @param {number} questionTime in seconds
*/
onCountdownTick(questionTime) {
+ const quizInfo = document.querySelector(selectors.quiz.info);
this.countdownTimeLeft--;
if (this.countdownTimeLeft <= 0) {
clearInterval(this.countdownInterval);
this.countdownInterval = 0;
this.startAttempt(questionTime);
} else if (this.countdownTimeLeft !== 0) {
- setText(Quiz.info, 'question_will_start_in_x_seconds', 'jazzquiz', this.countdownTimeLeft);
+ setText(quizInfo, 'question_will_start_in_x_seconds', 'jazzquiz', this.countdownTimeLeft);
} else {
- setText(Quiz.info, 'question_will_start_now');
+ setText(quizInfo, 'question_will_start_now');
}
}
@@ -166,7 +158,7 @@ export class Question {
* @param {number} questionTime
*/
startAttempt(questionTime) {
- Quiz.hide(Quiz.info);
+ document.querySelector(selectors.quiz.info).classList.add('hidden');
this.refresh();
// Set this to true so that we don't keep calling this over and over.
this.isRunning = true;
@@ -181,7 +173,7 @@ export class Question {
}
static isLoaded() {
- return Question.box.html() !== '';
+ return document.querySelector(selectors.question.box).innerHTML.length > 0;
}
}
diff --git a/amd/src/selectors.js b/amd/src/selectors.js
index f677d42..457266c 100644
--- a/amd/src/selectors.js
+++ b/amd/src/selectors.js
@@ -33,12 +33,27 @@ export default {
info: '#jazzquiz_info_container',
responded: '#jazzquiz_responded_container',
responses: '#jazzquiz_responses_container',
+ responseContainer: '.jazzquiz-response-container',
responseInfo: '#jazzquiz_response_info_container',
undoMergeButton: '#jazzquiz_undo_merge',
showNormalResultsButton: '#review_show_normal_results',
showVoteResultsButton: '#review_show_vote_results',
mergeInto: '.merge-into',
mergeFrom: '.merge-from',
+ saveVoteButton: '#jazzquiz_save_vote',
+ selectVoteButton: '.jazzquiz-select-vote',
+ controls: '#jazzquiz_controls',
+ controlsBox: '#jazzquiz_controls_box',
+ controlButtons: '.quiz-control-buttons',
+ sideContainer: '#jazzquiz_side_container',
+ correctAnswerContainer: '#jazzquiz_correct_answer_container',
+ controlButton: '.jazzquiz-control-button',
+ selectedVoteOption: '.selected-vote-option',
+ reportOverviewResponded: '#report_overview_responded',
+ reportOverviewResponses: '#report_overview_responses',
+ studentsJoinedCounter: '#jazzquiz_students_joined_counter',
+ startQuizButton: '#jazzquiz_start_quiz_button',
+ exitQuizButton: '#jazzquiz_exit_button',
},
edit: {
addSelectedQuestions: '.jazzquiz-add-selected-questions',
diff --git a/amd/src/student.js b/amd/src/student.js
index 469a0b7..6d4644c 100755
--- a/amd/src/student.js
+++ b/amd/src/student.js
@@ -21,13 +21,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import $ from 'jquery';
-import Jazz from 'mod_jazzquiz/core';
-
-const Quiz = Jazz.Quiz;
-const Question = Jazz.Question;
-const Ajax = Jazz.Ajax;
-const setText = Jazz.setText;
+import {Quiz, setText} from 'mod_jazzquiz/core';
+import selectors from 'mod_jazzquiz/selectors';
+import {addMathjaxElement, renderMaximaEquation} from 'mod_jazzquiz/math_rendering';
class Student {
@@ -37,23 +33,33 @@ class Student {
constructor(quiz) {
this.quiz = quiz;
this.voteAnswer = undefined;
- let self = this;
- $(document).on('submit', '#jazzquiz_question_form', function(event) {
- event.preventDefault();
- self.submitAnswer();
- }).on('click', '#jazzquiz_save_vote', function() {
- self.saveVote();
- }).on('click', '.jazzquiz-select-vote', function() {
- self.voteAnswer = this.value;
+
+ document.addEventListener('submit', event => {
+ const questionForm = event.target.closest(selectors.question.form);
+ if (questionForm) {
+ event.preventDefault();
+ this.submitAnswer();
+ }
+ });
+
+ document.addEventListener('click', event => {
+ const saveVoteButton = event.target.closest(selectors.quiz.saveVoteButton);
+ if (saveVoteButton) {
+ this.saveVote();
+ }
+ const selectVoteButton = event.target.closest(selectors.quiz.selectVoteButton);
+ if (selectVoteButton) {
+ this.voteAnswer = selectVoteButton.value;
+ }
});
}
onNotRunning() {
- setText(Quiz.info, 'instructions_for_student');
+ setText(document.querySelector(selectors.quiz.info), 'instructions_for_student');
}
onPreparing() {
- setText(Quiz.info, 'wait_for_instructor');
+ setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');
}
onRunning(data) {
@@ -62,7 +68,7 @@ class Student {
}
const started = this.quiz.question.startCountdown(data.questiontime, data.delay);
if (!started) {
- setText(Quiz.info, 'wait_for_instructor');
+ setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');
}
}
@@ -70,8 +76,8 @@ class Student {
this.quiz.question.isVoteRunning = false;
this.quiz.question.isRunning = false;
this.quiz.question.hideTimer();
- Quiz.hide(Question.box);
- setText(Quiz.info, 'wait_for_instructor');
+ document.querySelector(selectors.question.box).classList.add('hidden');
+ setText(document.querySelector(selectors.quiz.info), 'wait_for_instructor');
}
onSessionClosed() {
@@ -82,20 +88,21 @@ class Student {
if (this.quiz.question.isVoteRunning) {
return;
}
- Quiz.info.html(data.html);
- Quiz.show(Quiz.info);
- const options = data.options;
- for (let i = 0; i < options.length; i++) {
- Quiz.addMathjaxElement($('#' + options[i].content_id), options[i].text);
- if (options[i].question_type === 'stack') {
- Quiz.renderMaximaEquation(options[i].text, options[i].content_id);
+ const quizInfo = document.querySelector(selectors.quiz.info);
+ quizInfo.innerHTML = data.html;
+ quizInfo.classList.remove('hidden');
+ quizInfo.querySelectorAll('.jazzquiz-select-vote-label').forEach(label => {
+ const attemptSpan = label.querySelector('.jazzquiz-select-vote-attempt');
+ addMathjaxElement(attemptSpan, attemptSpan.innerHTML);
+ if (label.dataset.qtype === 'stack') {
+ renderMaximaEquation(attemptSpan.innerHTML, attemptSpan.id);
}
- }
+ });
this.quiz.question.isVoteRunning = true;
}
onTimerTick(timeLeft) {
- setText(Question.timer, 'question_will_end_in_x_seconds', 'jazzquiz', timeLeft);
+ setText(document.querySelector(selectors.question.timer), 'question_will_end_in_x_seconds', 'jazzquiz', timeLeft);
}
/**
@@ -111,7 +118,7 @@ class Student {
// eslint-disable-next-line no-undef
tinyMCE.triggerSave();
}
- const serialized = Question.form.serializeArray();
+ const serialized = document.querySelector(selectors.question.form).serializeArray();
let data = {};
for (let name in serialized) {
if (serialized.hasOwnProperty(name)) {
@@ -119,10 +126,12 @@ class Student {
}
}
Ajax.post('save_question', data, data => {
+ const quizInfo = document.querySelector(selectors.quiz.info);
if (data.feedback.length > 0) {
- Quiz.show(Quiz.info.html(data.feedback));
+ quizInfo.innerHTML = data.feedback;
+ quizInfo.classList.remove('hidden');
} else {
- setText(Quiz.info, 'wait_for_instructor');
+ setText(quizInfo, 'wait_for_instructor');
}
this.quiz.question.isSaving = false;
if (!this.quiz.question.isRunning) {
@@ -131,7 +140,7 @@ class Student {
if (this.quiz.question.isVoteRunning) {
return;
}
- Quiz.hide(Question.box);
+ document.querySelector(selectors.question.box).classList.add('hidden');
this.quiz.question.hideTimer();
}).fail(() => {
this.quiz.question.isSaving = false;
@@ -140,10 +149,11 @@ class Student {
saveVote() {
Ajax.post('save_vote', {vote: this.voteAnswer}, data => {
+ const quizInfo = document.querySelector(selectors.quiz.info);
if (data.status === 'success') {
- setText(Quiz.info, 'wait_for_instructor');
+ setText(quizInfo, 'wait_for_instructor');
} else {
- setText(Quiz.info, 'you_already_voted');
+ setText(quizInfo, 'you_already_voted');
}
});
}
diff --git a/classes/exporter.php b/classes/exporter.php
index 2aa47a7..893f43e 100755
--- a/classes/exporter.php
+++ b/classes/exporter.php
@@ -84,7 +84,7 @@ public function export_session(jazzquiz_session $session, array $quizattempts):
'answers' => $answers,
];
}
- $name = 'session_' . $session->data->id . '_' . $session->data->name;
+ $name = 'session_' . $session->id . '_' . $session->name;
return [$name, $slots, $users];
}
@@ -131,7 +131,7 @@ public function export_session_question(jazzquiz_session $session, jazzquiz_atte
$session->load_attempts();
$responses = $session->get_question_results_list($slot);
$responses = $responses['responses'];
- $name = 'session_ ' . $session->data->id . '_' . $session->data->name . '_' . $question->name;
+ $name = 'session_ ' . $session->id . '_' . $session->name . '_' . $question->name;
return [$name, $question->questiontext, $responses];
}
@@ -157,7 +157,7 @@ public function export_session_question_csv(jazzquiz_session $session, jazzquiz_
* @param jazzquiz_session $session
*/
public function export_attendance_csv(jazzquiz_session $session): void {
- $name = $session->data->id . '_' . $session->data->name;
+ $name = $session->id . '_' . $session->name;
$attendances = $session->get_attendances();
$this->csv_file("session_{$name}_attendance");
echo "IdNumber\tFirst Name\tLast Name\tResponses\r\n";
diff --git a/classes/external/get_vote_options.php b/classes/external/get_vote_options.php
new file mode 100644
index 0000000..938bc1f
--- /dev/null
+++ b/classes/external/get_vote_options.php
@@ -0,0 +1,98 @@
+.
+
+declare(strict_types=1);
+
+namespace mod_jazzquiz\external;
+
+use context_module;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use mod_jazzquiz\jazzquiz;
+use mod_jazzquiz\jazzquiz_session;
+use moodle_exception;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/externallib.php');
+
+/**
+ * Get possible vote options, given that the session is currently running a vote.
+ */
+class get_vote_options extends external_api {
+
+ /**
+ * Get the function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT, 'Course module id'),
+ 'sessionid' => new external_value(PARAM_INT, 'Session id'),
+ ]);
+ }
+
+ /**
+ * Get the vote options.
+ *
+ * @param int $cmid Course module id
+ * @param int $sessionid Session id
+ */
+ public static function execute(int $cmid, int $sessionid): array {
+ global $DB;
+ self::validate_parameters(self::execute_parameters(), ['cmid' => $cmid, 'sessionid' => $sessionid]);
+ self::validate_context(context_module::instance($cmid));
+ $session = new jazzquiz_session(null, $sessionid);
+ if (!$session->sessionopen) {
+ throw new moodle_exception('Session is closed.');
+ }
+ $session->load_my_attempt();
+ if (!$session->myattempt || !$session->myattempt->is_active()) {
+ throw new moodle_exception('Session is closed.');
+ }
+ if ($session->status !== jazzquiz_session::STATUS_VOTING) {
+ return ['html' => ''];
+ }
+ $options = [];
+ $i = 0;
+ foreach ($DB->get_records('jazzquiz_votes', ['sessionid' => $session->id]) as $voteoption) {
+ $options[] = [
+ 'index' => $i,
+ 'voteid' => $voteoption->id,
+ 'qtype' => $voteoption->qtype,
+ 'text' => $voteoption->attempt,
+ ];
+ $i++;
+ }
+ $jazzquiz = new jazzquiz($cmid);
+ return ['html' => $jazzquiz->renderer->render_from_template('jazzquiz/vote', ['options' => $options])];
+ }
+
+ /**
+ * Describe what we return.
+ *
+ * @return external_single_structure
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'html' => new external_value(PARAM_TEXT, 'HTML for vote options', VALUE_REQUIRED),
+ ]);
+ }
+
+}
diff --git a/classes/external/poll.php b/classes/external/poll.php
new file mode 100644
index 0000000..74c4c03
--- /dev/null
+++ b/classes/external/poll.php
@@ -0,0 +1,111 @@
+.
+
+declare(strict_types=1);
+
+namespace mod_jazzquiz\external;
+
+use context_module;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use mod_jazzquiz\jazzquiz;
+use mod_jazzquiz\jazzquiz_session;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/externallib.php');
+
+/**
+ * Poll session.
+ */
+class poll extends external_api {
+
+ /**
+ * Get the function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT, 'Course module id'),
+ 'sessionid' => new external_value(PARAM_INT, 'Session id'),
+ ]);
+ }
+
+ /**
+ * Poll session.
+ *
+ * @param int $cmid Course module id
+ * @param int $sessionid Session id
+ */
+ public static function execute(int $cmid, int $sessionid): array {
+ global $DB;
+ self::validate_parameters(self::execute_parameters(), ['cmid' => $cmid, 'sessionid' => $sessionid]);
+ self::validate_context(context_module::instance($cmid));
+ $jazzquiz = new jazzquiz($cmid);
+ $session = new jazzquiz_session(null, $sessionid);
+ if (!$session->sessionopen) {
+ return ['status' => jazzquiz_session::STATUS_SESSIONCLOSED];
+ }
+ $session->load_my_attempt();
+ if (!$session->myattempt || !$session->myattempt->is_active()) {
+ return ['status' => jazzquiz_session::STATUS_SESSIONCLOSED];
+ }
+ switch ($session->status) {
+ // Just a generic response with the state.
+ case jazzquiz_session::STATUS_NOTRUNNING:
+ if ($jazzquiz->is_instructor()) {
+ $session = new jazzquiz_session(null, $session->id);
+ $session->load_attempts();
+ return ['status' => $session->status, 'student_count' => $session->get_student_count()];
+ } else {
+ return ['status' => $session->status];
+ }
+
+ case jazzquiz_session::STATUS_PREPARING:
+ case jazzquiz_session::STATUS_REVIEWING:
+ return ['status' => $session->status, 'slot' => $session->slot];
+
+ case jazzquiz_session::STATUS_VOTING:
+ return ['status' => $session->status];
+
+ // Send the currently active question.
+ case jazzquiz_session::STATUS_RUNNING:
+ return [
+ 'status' => $session->status,
+ 'questiontime' => $session->currentquestiontime,
+ 'delay' => $session->nextstarttime - time(),
+ ];
+
+ // This should not be reached, but if it ever is, let's just assume the quiz is not running.
+ default:
+ return ['status' => jazzquiz_session::STATUS_NOTRUNNING];
+ }
+ }
+
+ /**
+ * Describe what we return.
+ *
+ * @return external_single_structure
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'status' => new external_value(PARAM_TEXT, 'Current session status', VALUE_REQUIRED),
+ ]);
+ }
+}
diff --git a/classes/jazzquiz.php b/classes/jazzquiz.php
index 0b44f86..40a4d20 100755
--- a/classes/jazzquiz.php
+++ b/classes/jazzquiz.php
@@ -141,15 +141,8 @@ public function get_display_options(bool $review, string|stdClass $reviewoptions
*/
public function load_open_session(): ?jazzquiz_session {
global $DB;
- $sessions = $DB->get_records('jazzquiz_sessions', [
- 'jazzquizid' => $this->data->id,
- 'sessionopen' => 1,
- ], 'id');
- if (empty($sessions)) {
- return null;
- }
- $session = reset($sessions);
- return new jazzquiz_session($this, $session->id);
+ $session = $DB->get_record('jazzquiz_sessions', ['jazzquizid' => $this->data->id, 'sessionopen' => 1]);
+ return empty($session) ? null : new jazzquiz_session($session);
}
/**
@@ -159,10 +152,7 @@ public function load_open_session(): ?jazzquiz_session {
*/
public function is_session_open(): bool {
global $DB;
- return $DB->record_exists('jazzquiz_sessions', [
- 'jazzquizid' => $this->data->id,
- 'sessionopen' => 1,
- ]);
+ return $DB->record_exists('jazzquiz_sessions', ['jazzquizid' => $this->data->id, 'sessionopen' => 1]);
}
/**
@@ -182,7 +172,7 @@ public function create_session(string $name, int $anonymity, bool $allowguests):
$session->name = $name;
$session->jazzquizid = $this->data->id;
$session->sessionopen = 1;
- $session->status = 'notrunning';
+ $session->status = jazzquiz_session::STATUS_NOTRUNNING;
$session->slot = 0;
$session->anonymity = $anonymity;
$session->showfeedback = false;
diff --git a/classes/jazzquiz_attempt.php b/classes/jazzquiz_attempt.php
index 133c020..74a6962 100755
--- a/classes/jazzquiz_attempt.php
+++ b/classes/jazzquiz_attempt.php
@@ -46,8 +46,8 @@ class jazzquiz_attempt {
/** The attempt is finished. */
const FINISHED = 30;
- /** @var int The jazzquiz attempt id */
- public int $id;
+ /** @var ?int The jazzquiz attempt id */
+ public ?int $id = null;
/** @var int The jazzquiz session id */
public int $sessionid;
@@ -82,9 +82,17 @@ class jazzquiz_attempt {
/**
* Constructor.
*
- * @param stdClass $record
+ * @param ?stdClass $record database record
+ * @param ?int $id attempt id to load record for, will always override $record parameter
*/
- private function __construct(stdClass $record) {
+ public function __construct(?stdClass $record = null, ?int $id = null) {
+ global $DB;
+ if ($id !== null) {
+ $record = $DB->get_record('jazzquiz_attempts', ['id' => $id], '*', MUST_EXIST);
+ }
+ if (!$record) {
+ return;
+ }
$this->id = $record->id;
$this->sessionid = $record->sessionid;
$this->userid = $record->userid;
@@ -180,7 +188,7 @@ public static function create(jazzquiz_session $session): jazzquiz_attempt {
// TODO: Don't suppress the error if it becomes possible to save QUBAs without slots.
@question_engine::save_questions_usage_by_activity($quba);
$id = $DB->insert_record('jazzquiz_attempts', [
- 'sessionid' => $session->data->id,
+ 'sessionid' => $session->id,
'userid' => isguestuser($USER->id) ? null : $USER->id,
'questionengid' => $quba->get_id(),
'guestsession' => isguestuser($USER->id) ? $USER->sesskey : null,
@@ -190,23 +198,11 @@ public static function create(jazzquiz_session $session): jazzquiz_attempt {
'timefinish' => null,
'timemodified' => time(),
]);
- $attempt = self::get_by_id($id);
+ $attempt = new jazzquiz_attempt(null, $id);
$attempt->create_missing_attempts($session);
return $attempt;
}
- /**
- * Helper function to properly load a JazzQuiz attempt by its ID.
- *
- * @param int $id
- * @return jazzquiz_attempt
- */
- public static function get_by_id(int $id): jazzquiz_attempt {
- global $DB;
- $record = $DB->get_record('jazzquiz_attempts', ['id' => $id], '*', MUST_EXIST);
- return new jazzquiz_attempt($record);
- }
-
/**
* Helper function to properly load a JazzQuiz attempt for the current user in a session.
*
@@ -220,7 +216,7 @@ public static function get_by_session_for_current_user(jazzquiz_session $session
WHERE sessionid = :sessionid
AND (userid = :userid OR guestsession = :sesskey)';
$record = $DB->get_record_sql($sql, [
- 'sessionid' => $session->data->id,
+ 'sessionid' => $session->id,
'userid' => $USER->id,
'sesskey' => $USER->sesskey,
]);
@@ -332,14 +328,11 @@ public function get_response_data(int $slot): array {
}
/**
- * Closes the attempt.
- *
- * @param jazzquiz $jazzquiz
+ * Close attempt.
*/
- public function close_attempt(jazzquiz $jazzquiz): void {
+ public function close_attempt(): void {
$this->quba->finish_all_questions(time());
- // We want the instructor to remain in preview mode.
- if (!$jazzquiz->is_instructor()) {
+ if ($this->status !== self::PREVIEW) {
$this->status = self::FINISHED;
}
$this->timefinish = time();
diff --git a/classes/jazzquiz_session.php b/classes/jazzquiz_session.php
index 4e69472..e2ece93 100755
--- a/classes/jazzquiz_session.php
+++ b/classes/jazzquiz_session.php
@@ -31,47 +31,130 @@
*/
class jazzquiz_session {
- /** @var jazzquiz $jazzquiz */
- public jazzquiz $jazzquiz;
+ /** The session has been created, but the instructor has not started the quiz yet. */
+ const STATUS_NOTRUNNING = 'notrunning';
- /** @var stdClass $data The jazzquiz_session database table row */
- public stdClass $data;
+ /** The quiz has been started, but no questions have been asked yet. */
+ const STATUS_PREPARING = 'preparing';
+
+ /** A question has been started, and students are submitting their attempts. */
+ const STATUS_RUNNING = 'running';
+
+ /** The question has ended, and the instructor can review attempts and start a vote. */
+ const STATUS_REVIEWING = 'reviewing';
+
+ /** There is currently a vote running on the answers of the previous question. */
+ const STATUS_VOTING = 'voting';
+
+ /** The session has been closed, and there will be no more questions or answers. */
+ const STATUS_SESSIONCLOSED = 'sessionclosed';
+
+ /** @var int|null The id for this session */
+ public ?int $id = null;
+
+ /** @var int The jazzquiz instance this session belongs to */
+ public int $jazzquizid;
+
+ /** @var string The name for this session */
+ public string $name;
+
+ /** @var int Whether the session is open */
+ public int $sessionopen;
+
+ /** @var bool Whether students should receive feedback after answering */
+ public bool $showfeedback;
+
+ /** @var string The current state of the session */
+ public string $status;
+
+ /** @var ?int This is which JazzQuiz question was last run. Not related to session questions. */
+ public ?int $slot = null;
+
+ /** @var ?int The time the current question is allowed in seconds */
+ public ?int $currentquestiontime = null;
+
+ /** @var ?int The timestamp of the next start time */
+ public ?int $nextstarttime = null;
+
+ /** @var int Which anonymity we use for this session */
+ public int $anonymity;
+
+ /** @var int If we allow guests */
+ public int $allowguests;
+
+ /** @var int Timestamp for when this session was created. */
+ public int $created;
/** @var jazzquiz_attempt[] */
- public array $attempts;
+ public array $attempts = [];
/** @var ?jazzquiz_attempt The current user's quiz attempt */
- public ?jazzquiz_attempt $myattempt;
+ public ?jazzquiz_attempt $myattempt = null;
/** @var stdClass[] Questions in this session */
- public array $questions;
+ public array $questions = [];
/**
* Constructor.
*
- * @param jazzquiz $jazzquiz
- * @param int $sessionid
+ * @param ?stdClass $record database record
+ * @param ?int $id session id to load record for, will always override $record parameter
*/
- public function __construct(jazzquiz $jazzquiz, int $sessionid) {
+ public function __construct(?stdClass $record = null, ?int $id = null) {
global $DB;
- $this->jazzquiz = $jazzquiz;
- $this->attempts = [];
- $this->myattempt = null;
- $this->questions = [];
- $this->data = $DB->get_record('jazzquiz_sessions', [
- 'jazzquizid' => $this->jazzquiz->data->id,
- 'id' => $sessionid,
- ], '*', MUST_EXIST);
+ if ($id !== null) {
+ $record = $DB->get_record('jazzquiz_sessions', ['id' => $id], '*', MUST_EXIST);
+ }
+ if (!$record) {
+ return;
+ }
+ $this->id = $record->id ?? null;
+ $this->jazzquizid = $record->jazzquizid;
+ $this->name = $record->name;
+ $this->sessionopen = $record->sessionopen;
+ $this->showfeedback = $record->showfeedback;
+ $this->status = $record->status;
+ $this->slot = $record->slot;
+ $this->currentquestiontime = $record->currentquestiontime;
+ $this->nextstarttime = $record->nextstarttime;
+ $this->anonymity = $record->anonymity;
+ $this->allowguests = $record->allowguests;
+ $this->created = $record->created;
+ }
+
+ /**
+ * Returns the current local record.
+ *
+ * @return stdClass
+ */
+ public function get_record(): stdClass {
+ $record = new stdClass();
+ if ($this->id) {
+ $record->id = $this->id;
+ }
+ $record->jazzquizid = $this->jazzquizid;
+ $record->name = $this->name;
+ $record->sessionopen = $this->sessionopen;
+ $record->showfeedback = $this->showfeedback;
+ $record->status = $this->status;
+ $record->slot = $this->slot;
+ $record->currentquestiontime = $this->currentquestiontime;
+ $record->nextstarttime = $this->nextstarttime;
+ $record->anonymity = $this->anonymity;
+ $record->allowguests = $this->allowguests;
+ $record->created = $this->created;
+ return $record;
}
/**
* Get array of unasked slots.
*
+ * @param jazzquiz_question[] $questions
* @return int[] question slots.
*/
- public function get_unasked_slots(): array {
+ public function get_unasked_slots(array $questions): array {
$slots = [];
- foreach ($this->jazzquiz->questions as $question) {
+ foreach ($questions as $question) {
$asked = false;
foreach ($this->questions as $sessionquestion) {
if ($sessionquestion->questionid == $question->data->questionid) {
@@ -92,7 +175,7 @@ public function get_unasked_slots(): array {
* @return bool
*/
private function requires_anonymous_answers(): bool {
- return $this->data->anonymity != 3;
+ return $this->anonymity != 3;
}
/**
@@ -101,7 +184,7 @@ private function requires_anonymous_answers(): bool {
* @return bool
*/
private function requires_anonymous_attendance(): bool {
- return $this->data->anonymity == 2;
+ return $this->anonymity == 2;
}
/**
@@ -109,10 +192,10 @@ private function requires_anonymous_attendance(): bool {
*/
public function save(): void {
global $DB;
- if (isset($this->data->id)) {
- $DB->update_record('jazzquiz_sessions', $this->data);
+ if ($this->id !== null) {
+ $DB->update_record('jazzquiz_sessions', $this->get_record());
} else {
- $this->data->id = $DB->insert_record('jazzquiz_sessions', $this->data);
+ $this->id = $DB->insert_record('jazzquiz_sessions', $this->get_record());
}
}
@@ -194,7 +277,7 @@ public function user_idnumber_for_attendance(?int $userid): string {
*/
private function anonymize_attendance(): void {
global $DB;
- $attendances = $DB->get_records('jazzquiz_attendance', ['sessionid' => $this->data->id]);
+ $attendances = $DB->get_records('jazzquiz_attendance', ['sessionid' => $this->id]);
foreach ($attendances as $attendance) {
$attendance->userid = null;
$DB->update_record('jazzquiz_attendance', $attendance);
@@ -222,12 +305,12 @@ private function anonymize_users(): void {
*/
public function end_session(): void {
$this->anonymize_users();
- $this->data->status = 'notrunning';
- $this->data->sessionopen = 0;
- $this->data->currentquestiontime = null;
- $this->data->nextstarttime = null;
+ $this->status = self::STATUS_NOTRUNNING;
+ $this->sessionopen = 0;
+ $this->currentquestiontime = null;
+ $this->nextstarttime = null;
foreach ($this->attempts as $attempt) {
- $attempt->close_attempt($this->jazzquiz);
+ $attempt->close_attempt();
}
$this->save();
}
@@ -242,12 +325,9 @@ public function end_session(): void {
public function merge_responses(int $slot, string $from, string $into): void {
global $DB;
$merge = new stdClass();
- $merge->sessionid = $this->data->id;
+ $merge->sessionid = $this->id;
$merge->slot = $slot;
- $merge->ordernum = count($DB->get_records('jazzquiz_merges', [
- 'sessionid' => $this->data->id,
- 'slot' => $slot,
- ]));
+ $merge->ordernum = $DB->count_records('jazzquiz_merges', ['sessionid' => $this->id, 'slot' => $slot]);
$merge->original = $from;
$merge->merged = $into;
$DB->insert_record('jazzquiz_merges', $merge);
@@ -261,7 +341,7 @@ public function merge_responses(int $slot, string $from, string $into): void {
public function undo_merge(int $slot): void {
global $DB;
$merge = $DB->get_records('jazzquiz_merges', [
- 'sessionid' => $this->data->id,
+ 'sessionid' => $this->id,
'slot' => $slot,
], 'ordernum desc', '*', 0, 1);
if (count($merge) > 0) {
@@ -279,10 +359,7 @@ public function undo_merge(int $slot): void {
*/
public function get_merged_responses(int $slot, array $responses): array {
global $DB;
- $merges = $DB->get_records('jazzquiz_merges', [
- 'sessionid' => $this->data->id,
- 'slot' => $slot,
- ]);
+ $merges = $DB->get_records('jazzquiz_merges', ['sessionid' => $this->id, 'slot' => $slot]);
$count = 0;
foreach ($merges as $merge) {
foreach ($responses as &$response) {
@@ -300,17 +377,18 @@ public function get_merged_responses(int $slot, array $responses): array {
*
* @param int $questionid (from question bank)
* @param int $questiontime in seconds ("<0" => no time, "0" => default)
+ * @param int $waitforquestiontime seconds until next question
* @return array $success, $question_time
*/
- public function start_question(int $questionid, int $questiontime): array {
+ public function start_question(int $questionid, int $questiontime, int $waitforquestiontime): array {
global $DB;
$transaction = $DB->start_delegated_transaction();
$sessionquestion = new stdClass();
- $sessionquestion->sessionid = $this->data->id;
+ $sessionquestion->sessionid = $this->id;
$sessionquestion->questionid = $questionid;
$sessionquestion->questiontime = $questiontime;
- $sessionquestion->slot = count($DB->get_records('jazzquiz_session_questions', ['sessionid' => $this->data->id])) + 1;
+ $sessionquestion->slot = count($DB->get_records('jazzquiz_session_questions', ['sessionid' => $this->id])) + 1;
$sessionquestion->id = $DB->insert_record('jazzquiz_session_questions', $sessionquestion);
$this->questions[$sessionquestion->slot] = $sessionquestion;
@@ -319,8 +397,8 @@ public function start_question(int $questionid, int $questiontime): array {
$attempt->save();
}
- $this->data->currentquestiontime = $questiontime;
- $this->data->nextstarttime = time() + $this->jazzquiz->data->waitforquestiontime;
+ $this->currentquestiontime = $questiontime;
+ $this->nextstarttime = time() + $waitforquestiontime;
$this->save();
$transaction->allow_commit();
@@ -360,7 +438,7 @@ public function update_attendance_for_current_user(): void {
return;
}
$attendance = $DB->get_record('jazzquiz_attendance', [
- 'sessionid' => $this->data->id,
+ 'sessionid' => $this->id,
'userid' => $USER->id,
]);
if ($attendance) {
@@ -368,7 +446,7 @@ public function update_attendance_for_current_user(): void {
$DB->update_record('jazzquiz_attendance', $attendance);
} else {
$attendance = new stdClass();
- $attendance->sessionid = $this->data->id;
+ $attendance->sessionid = $this->id;
$attendance->userid = $USER->id;
$attendance->numresponses = $numresponses;
$DB->insert_record('jazzquiz_attendance', $attendance);
@@ -381,8 +459,8 @@ public function update_attendance_for_current_user(): void {
public function load_attempts(): void {
global $DB;
$this->attempts = [];
- foreach ($DB->get_records('jazzquiz_attempts', ['sessionid' => $this->data->id]) as $attempt) {
- $this->attempts[$attempt->id] = jazzquiz_attempt::get_by_id($attempt->id);
+ foreach ($DB->get_records('jazzquiz_attempts', ['sessionid' => $this->id]) as $record) {
+ $this->attempts[$record->id] = new jazzquiz_attempt($record);
}
}
@@ -398,7 +476,7 @@ public function load_my_attempt(): void {
*/
public function load_session_questions(): void {
global $DB;
- $this->questions = $DB->get_records('jazzquiz_session_questions', ['sessionid' => $this->data->id], 'slot');
+ $this->questions = $DB->get_records('jazzquiz_session_questions', ['sessionid' => $this->id], 'slot');
foreach ($this->questions as $question) {
unset($this->questions[$question->id]);
$this->questions[$question->slot] = $question;
@@ -441,7 +519,7 @@ public function get_student_count(): int {
*
* @return string HTML
*/
- public function get_question_right_response(): string {
+ public function get_question_right_response(jazzquiz $jazzquiz): string {
// Use the current user's attempt to render the question with the right response.
$quba = $this->myattempt->quba;
$slot = count($this->questions);
@@ -457,9 +535,9 @@ public function get_question_right_response(): string {
$reviewoptions->specificfeedback = 1;
$reviewoptions->generalfeedback = 1;
/** @var output\renderer $renderer */
- $renderer = $this->jazzquiz->renderer;
+ $renderer = $jazzquiz->renderer;
ob_start();
- $html = $renderer->render_question($this->jazzquiz, $this->myattempt->quba, $slot, true, $reviewoptions);
+ $html = $renderer->render_question($jazzquiz, $this->myattempt->quba, $slot, true, $reviewoptions);
$htmlechoed = ob_get_clean();
return $html . $htmlechoed;
}
@@ -499,7 +577,7 @@ public function get_question_results_list(int $slot): array {
public function get_attendances(): array {
global $DB;
$attendances = [];
- $records = $DB->get_records('jazzquiz_attendance', ['sessionid' => $this->data->id]);
+ $records = $DB->get_records('jazzquiz_attendance', ['sessionid' => $this->id]);
foreach ($records as $record) {
$attendances[] = [
'idnumber' => $this->user_idnumber_for_attendance($record->userid),
diff --git a/classes/jazzquiz_vote.php b/classes/jazzquiz_vote.php
index 6b6275c..ee0036e 100755
--- a/classes/jazzquiz_vote.php
+++ b/classes/jazzquiz_vote.php
@@ -52,10 +52,7 @@ public function __construct(int $sessionid, int $slot = 0) {
*/
public function get_results(): array {
global $DB;
- return $DB->get_records('jazzquiz_votes', [
- 'sessionid' => $this->sessionid,
- 'slot' => $this->slot,
- ]);
+ return $DB->get_records('jazzquiz_votes', ['sessionid' => $this->sessionid, 'slot' => $this->slot]);
}
/**
diff --git a/classes/local/page_requirements_diff.php b/classes/local/page_requirements_diff.php
index 48bcf65..caf6b98 100644
--- a/classes/local/page_requirements_diff.php
+++ b/classes/local/page_requirements_diff.php
@@ -24,7 +24,7 @@
* To load a question without refreshing the page, we need the JavaScript for the question.
* Moodle stores this in page_requirements_manager, but there is no way to read the JS that is required.
* This class takes in the manager and keeps the JS for when we want to get a diff.
- * NOTE: This class is placed here because it will only ever be used by renderer::render_question_form()
+ * NOTE: This will only ever be used by renderer::render_question_form()
* TODO: Look into removing this class in the future.
*
* @package mod_jazzquiz
diff --git a/classes/output/renderer.php b/classes/output/renderer.php
index 39a6f40..1bae2a5 100755
--- a/classes/output/renderer.php
+++ b/classes/output/renderer.php
@@ -91,9 +91,9 @@ public function continue_session_form(jazzquiz $jazzquiz): void {
public function join_quiz_form(student_start_form $form, jazzquiz_session $session): void {
$anonstr = ['', 'anonymous_answers_info', 'fully_anonymous_info', 'nonanonymous_session_info'];
echo $this->render_from_template('jazzquiz/join_session', [
- 'name' => $session->data->name,
+ 'name' => $session->name,
'started' => $session->myattempt !== false,
- 'anonymity_info' => get_string($anonstr[$session->data->anonymity], 'jazzquiz'),
+ 'anonymity_info' => get_string($anonstr[$session->anonymity], 'jazzquiz'),
'form' => $form->render(),
]);
}
@@ -123,31 +123,7 @@ public function guests_not_allowed(): void {
*/
public function render_quiz(jazzquiz_session $session): void {
$this->require_quiz($session);
- $buttons = function($buttons) {
- $result = [];
- foreach ($buttons as $button) {
- $result[] = [
- 'icon' => $button[0],
- 'id' => $button[1],
- 'text' => get_string($button[1], 'jazzquiz'),
- ];
- }
- return $result;
- };
echo $this->render_from_template('jazzquiz/quiz', [
- 'buttons' => $buttons([
- ['repeat', 'repoll'],
- ['bar-chart', 'vote'],
- ['edit', 'improvise'],
- ['bars', 'jump'],
- ['forward', 'next'],
- ['random', 'random'],
- ['close', 'end'],
- ['expand', 'fullscreen'],
- ['window-close', 'quit'],
- ['square-o', 'responses'],
- ['square-o', 'answer'],
- ]),
'instructor' => $session->jazzquiz->is_instructor(),
]);
}
@@ -221,10 +197,7 @@ public function get_select_session_context(moodle_url $url, array $sessions, int
];
}, $sessions),
'params' => array_map(function ($key, $value) {
- return [
- 'name' => $key,
- 'value' => $value,
- ];
+ return ['name' => $key, 'value' => $value];
}, array_keys($selecturl->params()), $selecturl->params()),
];
}
@@ -275,10 +248,11 @@ public function session_is_open_error(): void {
* View session report.
*
* @param jazzquiz_session $session
+ * @param jazzquiz $jazzquiz
* @param moodle_url $url
* @return array
*/
- public function view_session_report(jazzquiz_session $session, moodle_url $url): array {
+ public function view_session_report(jazzquiz_session $session, jazzquiz $jazzquiz, moodle_url $url): array {
$attempt = reset($session->attempts);
if (!$attempt) {
$strnoattempts = get_string('no_attempts_found', 'jazzquiz');
@@ -290,7 +264,7 @@ public function view_session_report(jazzquiz_session $session, moodle_url $url):
$qattempt = $attempt->quba->get_question_attempt($qubaslot);
$question = $qattempt->get_question();
$results = $session->get_question_results_list($qubaslot);
- list($results['responses'], $mergecount) = $session->get_merged_responses($qubaslot, $results['responses']);
+ [$results['responses'], $mergecount] = $session->get_merged_responses($qubaslot, $results['responses']);
$slots[] = [
'num' => $qubaslot,
'name' => str_replace('{IMPROV}', '', $question->name),
@@ -302,13 +276,12 @@ public function view_session_report(jazzquiz_session $session, moodle_url $url):
// TODO: Slots should not be passed as parameter to AMD module.
// It quickly gets over 1KB, which shows debug warning.
- $this->require_review($session, $slots);
+ $this->require_review($session, $jazzquiz, $slots);
$attendances = $session->get_attendances();
- $jazzquiz = $session->jazzquiz;
$sessions = $jazzquiz->get_sessions();
return [
- 'select_session' => $jazzquiz->renderer->get_select_session_context($url, $sessions, $session->data->id),
+ 'select_session' => $jazzquiz->renderer->get_select_session_context($url, $sessions, $session->id),
'session' => [
'slots' => $slots,
'students' => $attendances,
@@ -316,7 +289,7 @@ public function view_session_report(jazzquiz_session $session, moodle_url $url):
'count_answered' => count($attendances),
'cmid' => $jazzquiz->cm->id,
'quizid' => $jazzquiz->data->id,
- 'id' => $session->data->id,
+ 'id' => $session->id,
],
];
}
@@ -325,12 +298,12 @@ public function view_session_report(jazzquiz_session $session, moodle_url $url):
* Require the core javascript.
*
* @param jazzquiz_session $session
+ * @param jazzquiz $jazzquiz
*/
- public function require_core(jazzquiz_session $session): void {
+ public function require_core(jazzquiz_session $session, jazzquiz $jazzquiz): void {
$this->page->requires->js_call_amd('mod_jazzquiz/core', 'initialize', [
- $session->jazzquiz->cm->id,
- $session->jazzquiz->data->id,
- $session->data->id,
+ $jazzquiz->cm->id,
+ $session->id,
$session->myattempt?->id ?? 0,
sesskey(),
]);
@@ -341,11 +314,11 @@ public function require_core(jazzquiz_session $session): void {
*
* @param jazzquiz_session $session
*/
- public function require_quiz(jazzquiz_session $session): void {
- $this->require_core($session);
+ public function require_quiz(jazzquiz_session $session, jazzquiz $jazzquiz): void {
+ $this->require_core($session, $jazzquiz);
$this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton');
- if ($session->jazzquiz->is_instructor()) {
- $count = count($session->jazzquiz->questions);
+ if ($jazzquiz->is_instructor()) {
+ $count = count($jazzquiz->questions);
$params = [$count, false, []];
$this->page->requires->js_call_amd('mod_jazzquiz/instructor', 'initialize', $params);
} else {
@@ -369,11 +342,10 @@ public function require_edit(int $cmid): void {
* @param jazzquiz_session $session
* @param array $slots
*/
- public function require_review(jazzquiz_session $session, array $slots): void {
- $this->require_core($session);
- $count = count($session->jazzquiz->questions);
- $params = [$count, true, $slots];
- $this->page->requires->js_call_amd('mod_jazzquiz/instructor', 'initialize', $params);
+ public function require_review(jazzquiz_session $session, jazzquiz $jazzquiz, array $slots): void {
+ $this->require_core($session, $jazzquiz);
+ $count = count($jazzquiz->questions);
+ $this->page->requires->js_call_amd('mod_jazzquiz/instructor', 'initialize', [$count, true, $slots]);
}
}
diff --git a/db/services.php b/db/services.php
new file mode 100644
index 0000000..8dbca98
--- /dev/null
+++ b/db/services.php
@@ -0,0 +1,27 @@
+.
+
+defined('MOODLE_INTERNAL') || die();
+
+$functions = [
+ 'mod_jazzquiz_poll' => [
+ 'classname' => '',
+ 'description' => '',
+ 'type' => 'read',
+ 'ajax' => true,
+ 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
+ ]
+];
diff --git a/reports.php b/reports.php
index a895397..4f4f3b9 100755
--- a/reports.php
+++ b/reports.php
@@ -55,10 +55,10 @@ function jazzquiz_view_session_report(jazzquiz $jazzquiz, moodle_url $url, int $
}
$sessionid = current($sessions)->id;
}
- $session = new jazzquiz_session($jazzquiz, $sessionid);
+ $session = new jazzquiz_session(null, $sessionid);
$session->load_attempts();
$url->param('sessionid', $sessionid);
- $context = $jazzquiz->renderer->view_session_report($session, $url);
+ $context = $jazzquiz->renderer->view_session_report($session, $jazzquiz, $url);
echo $jazzquiz->renderer->render_from_template('jazzquiz/report', $context);
}
diff --git a/styles.css b/styles.css
index 0fa9d4c..e0282de 100755
--- a/styles.css
+++ b/styles.css
@@ -15,13 +15,6 @@
}
}
-#jazzquiz_loading {
- margin-left: auto;
- margin-right: auto;
- text-align: center;
- width: 30%;
-}
-
#jazzquiz_responses_container,
#jazzquiz_responded_container,
#jazzquiz_info_container {
@@ -192,26 +185,11 @@
font-style: italic;
}
-#jazzquiz_controls .btn {
- text-align: left;
- border: none;
- outline: none;
- border-radius: 0;
- background: #f9f9f9;
-}
-
.jazzquiz-fullscreen #jazzquiz_controls .btn {
font-size: 1.4vw;
padding: 0.6vw 1.2vw;
}
-#jazzquiz_controls .btn:hover,
-#jazzquiz_controls .btn.active {
- background: #29f;
- color: white;
- transition: none;
-}
-
@media all and (min-width: 1152px) {
.jazzquiz-fullscreen #jazzquiz_controls .btn {
font-size: 1.1rem;
@@ -236,12 +214,6 @@
width: 100%;
}
-#jazzquiz_control_quit,
-#jazzquiz_control_responses,
-#jazzquiz_control_answer {
- float: right;
-}
-
.merge-from {
background: #eee;
cursor: pointer;
@@ -397,36 +369,13 @@
cursor: pointer;
}
-#jazzquiz_response_info_container .btn.btn-primary,
-#report_overview_controls .btn.btn-primary,
-#jazzquiz_undo_merge,
-#jazzquiz_control_startquiz,
-#jazzquiz_control_exit {
- background: #eee;
- color: #444;
- border-radius: 0;
- border: 1px solid #aaa;
- outline: none;
-}
-
#jazzquiz_control_startquiz {
font-size: 2rem;
- padding: 16px 32px;
- margin: 16px;
-}
-
-#jazzquiz_control_startquiz:hover,
-#jazzquiz_control_exit:hover,
-#jazzquiz_undo_merge:hover,
-#report_overview_controls .btn.btn-primary:hover,
-#jazzquiz_response_info_container .btn.btn-primary:hover {
- background: #29f;
- color: #fff;
- transition: none;
+ padding: 1rem 2rem;
+ margin: 1rem;
}
#jazzquiz_control_exit {
- float: right;
margin: 16px;
font-size: 1.2rem;
}
@@ -444,3 +393,15 @@
.jazzquiz-box form#questionsubmit {
overflow-x: auto;
}
+
+#jazzquiz_start_quiz_wrapper {
+ display: grid;
+ grid-template-columns: max-content auto max-content;
+ gap: 1rem;
+ align-items: center;
+ width: 100%;
+}
+
+#jazzquiz_students_joined_counter {
+ margin: 0;
+}
diff --git a/templates/quiz.mustache b/templates/quiz.mustache
index e1e4d84..8b47ff1 100755
--- a/templates/quiz.mustache
+++ b/templates/quiz.mustache
@@ -21,14 +21,7 @@
Example context (json):
{
- "instructor": true,
- "buttons": [
- {
- "id": "improvise",
- "icon": "edit",
- "text": "Improvise"
- }
- ]
+ "instructor": true
}
}}
@@ -36,23 +29,54 @@
-
diff --git a/templates/vote.mustache b/templates/vote.mustache
new file mode 100644
index 0000000..1bf3827
--- /dev/null
+++ b/templates/vote.mustache
@@ -0,0 +1,45 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see
.
+}}
+{{!
+ @template mod_jazzquiz/vote
+
+ A vote on attempts from a question.
+
+ Example context (json):
+ {
+ "options": [
+ {
+ "index": 0,
+ "voteid": 4,
+ "qtype": "shortanswer",
+ "text": "Example attempt one"
+ },
+ {
+ "index": 1,
+ "voteid": 5,
+ "qtype": "shortanswer",
+ "text": "Example attempt two"
+ }
+ ]
+ }
+}}
+
+ {{#options}}
+ {{>jazzquiz/vote_option}}
+ {{/options}}
+
+
{{#str}}save, core{{/str}}
diff --git a/templates/vote_option.mustache b/templates/vote_option.mustache
new file mode 100644
index 0000000..f7982ed
--- /dev/null
+++ b/templates/vote_option.mustache
@@ -0,0 +1,33 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see
.
+}}
+{{!
+ @template mod_jazzquiz/vote_option
+
+ Option element in a vote.
+
+ Example context (json):
+ {
+ "index": 0,
+ "voteid": 4,
+ "qtype": "shortanswer",
+ "text": "Example attempt"
+ }
+}}
+
+
+ {{{text}}}
+
diff --git a/view.php b/view.php
index 6f39778..133df1c 100755
--- a/view.php
+++ b/view.php
@@ -165,7 +165,7 @@ function jazzquiz_view(): void {
* and teacher, but it cannot handle the case where guest
* access is allowed. Hence, if guests are allowed, no further check is made.
*/
- if ($session === null || $session->data->allowguests != 1) {
+ if ($session === null || $session->allowguests !== 1) {
try {
/*
* require_capability() throws an exception if the user does not