diff --git a/lib/questionlib.php b/lib/questionlib.php index bdedaaa0870..58b5155eee0 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -2083,16 +2083,31 @@ function question_format_grade($cmoptions, $grade) { */ function question_init_qengine_js() { global $CFG, $PAGE, $OUTPUT; - $config = array( - 'actionurl' => $CFG->wwwroot . '/question/toggleflag.php', - 'flagicon' => '' . $OUTPUT->pix_url('i/flagged'), - 'unflagicon' => '' . $OUTPUT->pix_url('i/unflagged'), - 'flagtooltip' => get_string('clicktoflag', 'question'), - 'unflagtooltip' => get_string('clicktounflag', 'question'), - 'flaggedalt' => get_string('flagged', 'question'), - 'unflaggedalt' => get_string('notflagged', 'question'), + static $done = false; + if ($done) { + return; + } + $module = array( + 'name' => 'core_question_flags', + 'fullpath' => '/question/flags.js', + 'requires' => array('base', 'dom', 'event-delegate', 'io-base'), ); - $PAGE->requires->data_for_js('qengine_config', $config); + $actionurl = $CFG->wwwroot . '/question/toggleflag.php'; + $flagattributes = array( + 0 => array( + 'src' => $OUTPUT->pix_url('i/unflagged') . '', + 'title' => get_string('clicktoflag', 'question'), + 'alt' => get_string('notflagged', 'question'), + ), + 1 => array( + 'src' => $OUTPUT->pix_url('i/flagged') . '', + 'title' => get_string('clicktounflag', 'question'), + 'alt' => get_string('flagged', 'question'), + ), + ); + $PAGE->requires->js_init_call('M.core_question_flags.init', + array($actionurl, $flagattributes), false, $module); + $done = true; } /// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS ////////////////////////////////// @@ -2109,12 +2124,10 @@ function question_init_qengine_js() { * @param array $states an array of question state objects, whose keys are question ids. * Must contain the state of all the questions in $questionlist */ -function get_html_head_contributions($questionlist, &$questions, &$states) { +function question_get_html_head_contributions($questionlist, &$questions, &$states) { global $CFG, $PAGE, $QTYPES; // The question engine's own JavaScript. - $PAGE->requires->yui2_lib('connection'); - $PAGE->requires->js('/question/qengine.js'); question_init_qengine_js(); // Anything that questions on this page need. @@ -2131,7 +2144,7 @@ function get_html_head_contributions($questionlist, &$questions, &$states) { * @param $question A question object. Only $question->qtype is used. * @return string Deprecated. Some HTML code that can go inside the head tag. */ -function get_editing_head_contributions($question) { +function question_get_editing_head_contributions($question) { global $QTYPES; $QTYPES[$question->qtype]->get_editing_head_contributions(); } diff --git a/mod/quiz/accessrules.php b/mod/quiz/accessrules.php index 34fcd96cfe0..51af1b047c4 100644 --- a/mod/quiz/accessrules.php +++ b/mod/quiz/accessrules.php @@ -176,8 +176,8 @@ class quiz_access_manager { /// Make sure the timer starts just above zero. If $timeleft was <= 0, then /// this will just have the effect of causing the quiz to be submitted immediately. $timerstartvalue = max($timeleft, 1); - $PAGE->requires->js_function_call('quiz_timer.initialise', - array(get_string('timesup', 'quiz'), $timerstartvalue)); + $PAGE->requires->js_init_call('M.mod_quiz.timer.init', + array($timerstartvalue), false, quiz_get_js_module()); } } @@ -207,26 +207,45 @@ class quiz_access_manager { * or start a new one. This affects whether a javascript alert is shown. */ public function print_start_attempt_button($canpreview, $buttontext, $unfinished) { - /// Do we need a confirm javascript alert? global $OUTPUT; - if ($unfinished) { - $strconfirmstartattempt = ''; - } else { - $strconfirmstartattempt = $this->confirm_start_attempt_message(); - } - /// Show the start button, in a div that is initially hidden. - echo '
'; - if ($this->securewindow_required($canpreview)) { - $this->_securewindowrule->print_start_attempt_button($buttontext, $strconfirmstartattempt); - } else { - $button = new single_button(new moodle_url($this->_quizobj->start_attempt_url(), array('cmid' => $this->_quizobj->get_cmid())), $buttontext); + $url = new moodle_url($this->_quizobj->start_attempt_url(), + array('cmid' => $this->_quizobj->get_cmid())); + $button = new single_button($url, $buttontext); + $button->class .= ' quizstartbuttondiv'; + + if (!$unfinished) { + $strconfirmstartattempt = $this->confirm_start_attempt_message(); if ($strconfirmstartattempt) { $button->add_confirm_action($strconfirmstartattempt); } - echo $OUTPUT->render($button); } - echo "
\n"; + + $warning = ''; + + if ($this->securewindow_required($canpreview)) { + $button->class .= ' quizsecuremoderequired'; + + $popupoptions = array( + 'left' => 0, + 'top' => 0, + 'fullscreen' => true, + 'scrollbars' => true, + 'resizeable' => false, + 'directories' => false, + 'toolbar' => false, + 'titlebar' => false, + 'location' => false, + 'status' => false, + 'menubar' => false, + ); + $button->popup_action(new popup_action('click', $url, 'quizpopup', $popupoptions)); + + $warning = html_writer::tag('noscript', + $OUTPUT->heading(get_string('noscript', 'quiz'))); + } + + echo $OUTPUT->render($button) . $warning; } /** @@ -249,7 +268,7 @@ class quiz_access_manager { $delay = 0; } echo $OUTPUT->box_end(); - $PAGE->requires->js_function_call('quiz_secure_window.close', array($url, $delay)); + $PAGE->requires->js_function_call('M.mod_quiz.secure_window.close', array($url, $delay)); echo $OUTPUT->footer(); die(); } else { @@ -259,7 +278,7 @@ class quiz_access_manager { /** * Print a control to finish the review. Normally this is just a link, but if we are - * in a secure window, it needs to be a button that does quiz_secure_window.close. + * in a secure window, it needs to be a button that does M.mod_quiz.secure_window.close. * * @param boolean $canpreview This affects whether we have to worry about secure window stuff. */ @@ -271,7 +290,7 @@ class quiz_access_manager { if ($this->securewindow_required($canpreview)) { $url = addslashes_js(htmlspecialchars($url)); $output .= '\n"; + "onclick=\"M.mod_quiz.secure_window.close('$url', 0)\" />\n"; } else { $output .= '' . get_string('finishreview', 'quiz') . "\n"; } @@ -699,45 +718,6 @@ class time_limit_access_rule extends quiz_access_rule_base { * A rule implementing the ipaddress check against the ->submet setting. */ class securewindow_access_rule extends quiz_access_rule_base { - private $windowoptions = "left=0, top=0, height='+window.screen.height+', width='+window.screen.width+', channelmode=yes, fullscreen=yes, scrollbars=yes, resizeable=no, directories=no, toolbar=no, titlebar=no, location=no, status=no, menubar=no"; - - /** - * Output the start attempt button. The button will initially be hidden, - * with JavaScript to reveal it, and a noscript tag saying that the quiz - * requires JavaScript. - * - * @param string $buttontext the desired button caption. - * @param string $strconfirmstartattempt optional message to diplay in - * a JavaScript altert before the button submits. - */ - public function print_start_attempt_button($buttontext, $strconfirmstartattempt) { - global $CFG, $SESSION, $PAGE, $OUTPUT; - - $attempturl = $this->_quizobj->start_attempt_url() . '?cmid=' . $this->_quizobj->get_cmid() . - '&sesskey=' . sesskey(); - $window = 'quizpopup'; - - if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) { - $attempturl = $SESSION->sid_process_url($attempturl); - } - - echo 'windowoptions');", '" />'; - - /// JavaScript to reveal the button. - echo html_writer::script('', $CFG->wwwroot.'/mod/quiz/quiz.js'); - echo html_writer::script(js_writer::function_call('reveal_start_button')); - - /// A noscript tag to explains that this quiz only works with JavaScript enabled. - echo '\n"; - } - /** * Make a link to the review page for an attempt. * @@ -765,14 +745,10 @@ class securewindow_access_rule extends quiz_access_rule_base { define('MESSAGE_WINDOW', true); $PAGE->set_title($title); $PAGE->set_cacheable(false); + $PAGE->add_body_class('quiz-secure-window'); + $PAGE->requires->js_init_call('M.mod_quiz.secure_window.init', null, false, + quiz_get_js_module()); echo $OUTPUT->header(); - echo "\n\n", '\n"; } } diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 80e8dac47cb..852dd458f24 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -88,6 +88,10 @@ /// Print the quiz page //////////////////////////////////////////////////////// + // Initialise the JavaScript. + $headtags = $attemptobj->get_html_head_contributions($page); + $PAGE->requires->js_init_call('M.mod_quiz.init_attempt_form', null, false, quiz_get_js_module()); + // Arrange for the navigation to be displayed. $navbc = $attemptobj->get_navigation_panel('quiz_attempt_nav_panel', $page); $firstregion = reset($PAGE->blocks->get_regions()); @@ -95,7 +99,6 @@ // Print the page header $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number()); - $headtags = $attemptobj->get_html_head_contributions($page); $PAGE->set_heading($attemptobj->get_course()->fullname); if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) { $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' . @@ -129,11 +132,6 @@ // Start the form echo '
', "\n"; - - // A quiz page with a lot of questions can take a long time to load, and we - // want the protection afforded by init_quiz_form immediately, so include the - // JS now. - echo html_writer::script(js_writer::function_call('init_quiz_form')); echo '
'; /// Print all the questions @@ -154,7 +152,7 @@ // Some hidden fields to trach what is going on. echo ''; echo ''; - echo ''; + echo ''; echo ''; // Add a hidden field with questionids. Do this at the end of the form, so diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 42c782c762d..30d66ce3141 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -273,7 +273,7 @@ class quiz { /** * @param integer $timenow the current time as a unix timestamp. - * @return object and instance of the quiz_access_manager class for this quiz at this time. + * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time. */ public function get_access_manager($timenow) { if (is_null($this->accessmanager)) { @@ -763,12 +763,7 @@ class quiz_attempt extends quiz { */ public function get_html_head_contributions($page = 'all') { global $PAGE; - // The JS does important things like navigation and so must be initialised - // as seen as possible, particularly if the page is loading slowly. - $PAGE->requires->yui2_lib('dom'); - $PAGE->requires->yui2_lib('event'); - $PAGE->requires->js('/mod/quiz/quiz.js', true); - get_html_head_contributions($this->get_question_ids($page), $this->questions, $this->states); + question_get_html_head_contributions($this->get_question_ids($page), $this->questions, $this->states); } /** @@ -776,7 +771,7 @@ class quiz_attempt extends quiz { * @param integer $questionid the question id. */ public function get_question_html_head_contributions($questionid) { - get_html_head_contributions(array($questionid), $this->questions, $this->states); + question_get_html_head_contributions(array($questionid), $this->questions, $this->states); } /** @@ -1129,7 +1124,7 @@ abstract class quiz_nav_panel_base { public function get_contents() { global $PAGE; - $PAGE->requires->js_function_call('quiz_init_nav_flags'); + $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module()); $content = ''; if ($this->attemptobj->get_quiz()->showuserpicture) { @@ -1168,7 +1163,6 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { $output = ''; $output .= '' . get_string('endtest', 'quiz') . ''; $output .= $this->attemptobj->get_timer_html(); - $output .= html_writer::script(js_writer::function_call('quiz_init_attempt_nav')); return $output; } } diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 2f871ff102c..6541be044f8 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -357,6 +357,7 @@ $string['formulaerror'] = 'Formula errors!'; $string['fractionsaddwrong'] = 'The positive grades you have chosen do not add up to 100%
Instead, they add up to {$a}%
Do you want to go back and fix this question?'; $string['fractionsnomax'] = 'One of the answers should be 100%, so that it is
possible to get a full grade for this question.
Do you want to go back and fix this question?'; $string['fromfile'] = 'from file:'; +$string['functiondisabledbysecuremode'] = 'That functionality is currently disabled'; $string['functiontakesatleasttwo'] = 'The function {$a} must have at least two arguments'; $string['functiontakesnoargs'] = 'The function {$a} does not take any arguments'; $string['functiontakesonearg'] = 'The function {$a} must have exactly one argument'; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 85ea4bf02d5..0685c6b871a 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -1251,3 +1251,15 @@ function quiz_error($quiz, $errorcode, $a = null) { function quiz_check_safe_browser() { return strpos($_SERVER['HTTP_USER_AGENT'], "SEB") !== false; } + +function quiz_get_js_module() { + return array( + 'name' => 'mod_quiz', + 'fullpath' => '/mod/quiz/module.js', + 'requires' => array('base', 'dom', 'event-delegate', 'event-key'), + 'strings' => array( + array('timesup', 'quiz'), + array('functiondisabledbysecuremode', 'quiz'), + ), + ); +} \ No newline at end of file diff --git a/mod/quiz/module.js b/mod/quiz/module.js new file mode 100644 index 00000000000..24b59f0c4f0 --- /dev/null +++ b/mod/quiz/module.js @@ -0,0 +1,220 @@ +/* + * JavaScript library for the quiz module. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +M.mod_quiz = M.mod_quiz || {}; + +M.mod_quiz.init_attempt_form = function(Y) { + Y.one('#responseform').setAttribute('autocomplete', 'off'); + Y.on('key', function (e) { + if (!e.target.test('a') && !e.target.test('input[type=submit]') && + !e.target.test('input[type=img]')) { + e.preventDefault(); + } + }, '#responseform', 'press:13'); + Y.on('submit', M.mod_quiz.timer.stop, '#responseform'); +} + +M.mod_quiz.init_review_form = function(Y) { + Y.all('.questionflagsavebutton').remove(); + + Y.on('key', function (e) { + if (!e.target.test('a') && !e.target.test('input[type=submit]') && + !e.target.test('input[type=img]')) { + e.preventDefault(); + } + }, '.questionflagsaveform', 'press:13'); + + Y.on('submit', function(e) { e.halt(); }, '.questionflagsaveform'); +} + +// Code for updating the countdown timer that is used on timed quizzes. +M.mod_quiz.timer = { + // YUI object. + Y: null, + + // Timestamp at which time runs out, according to the student's computer's clock. + endtime: 0, + + // This records the id of the timeout that updates the clock periodically, + // so we can cancel. + timeoutid: null, + + /** + * @param Y the YUI object + * @param timeleft, the time remaining, in seconds. + */ + init: function(Y, timeleft) { + M.mod_quiz.timer.Y = Y; + M.mod_quiz.timer.endtime = new Date().getTime() + timeleft*1000; + M.mod_quiz.timer.update(); + Y.one('#quiz-timer').setStyle('display', 'block'); + }, + + /** + * Stop the timer, if it is running. + */ + stop: function(e) { + if (M.mod_quiz.timer.timeoutid) { + clearTimeout(M.mod_quiz.timer.timeoutid); + } + }, + + /** + * Function to convert a number between 0 and 99 to a two-digit string. + */ + two_digit: function(num) { + if (num < 10) { + return '0' + num; + } else { + return num; + } + }, + + // Function to update the clock with the current time left, and submit the quiz if necessary. + update: function() { + var Y = M.mod_quiz.timer.Y; + var secondsleft = Math.floor((M.mod_quiz.timer.endtime - new Date().getTime())/1000); + + // If time has expired, Set the hidden form field that says time has expired. + if (secondsleft < 0) { + M.mod_quiz.timer.stop(null); + Y.one('#quiz-time-left').setContent(M.str.quiz.timesup); + var input = Y.one('input[name=timeup]'); + input.set('value', 1); + input.ancestor('form').submit(); + return; + } + + // If time has nearly expired, change the colour. + if (secondsleft < 100) { + Y.one('#quiz-timer').removeClass('timeleft' + (secondsleft + 2)) + .removeClass('timeleft' + (secondsleft + 1)) + .addClass('timeleft' + secondsleft); + } + + // Update the time display. + var hours = Math.floor(secondsleft/3600); + secondsleft -= hours*3600; + var minutes = Math.floor(secondsleft/60); + secondsleft -= minutes*60; + var seconds = secondsleft; + Y.one('#quiz-time-left').setContent('' + hours + ':' + + M.mod_quiz.timer.two_digit(minutes) + ':' + + M.mod_quiz.timer.two_digit(seconds)); + + // Arrange for this method to be called again soon. + M.mod_quiz.timer.timeoutid = setTimeout(M.mod_quiz.timer.update, 100); + } +}; + +M.mod_quiz.nav = M.mod_quiz.nav || {}; + +M.mod_quiz.nav.update_flag_state = function(attemptid, questionid, newstate) { + var Y = M.mod_quiz.nav.Y; + var navlink = Y.one('#quiznavbutton' + questionid); + navlink.removeClass('flagged'); + if (newstate == 1) { + navlink.addClass('flagged'); + } +}; + +M.mod_quiz.nav.init = function(Y) { + M.mod_quiz.nav.Y = Y; + + Y.all('#quiznojswarning').remove(); + + Y.delegate('click', function(e) { + if (this.hasClass('thispage')) { + return; + } + + e.preventDefault(e); + + var pageidmatch = this.get('href').match(/page=(\d+)/); + var pageno; + if (pageidmatch) { + pageno = pageidmatch[1]; + } else { + pageno = 0; + } + Y.one('#nextpagehiddeninput').set('value', pageno); + + var form = Y.one('#responseform'); + + var questionidmatch = this.get('href').match(/#q(\d+)/); + if (questionidmatch) { + form.set(action, form.get(action) + '#q' + questionidmatch[1]); + } + + form.submit(); + }, document.body, '.qnbutton'); + + if (Y.one('a.endtestlink')) { + Y.on('click', function(e) { + e.preventDefault(); + Y.one('#nextpagehiddeninput').set('value', -1); + Y.one('#responseform').submit(); + }, 'a.endtestlink'); + } + + if (M.core_question_flags) { + M.core_question_flags.add_listener(M.mod_quiz.nav.update_flag_state); + } +}; + +M.mod_quiz.secure_window = { + init: function(Y) { + if (window.location.href.substring(0,4) == 'file') { + window.location = 'about:blank'; + } + Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document.body, '*'); + Y.delegate('mousedown', M.mod_quiz.secure_window.prevent_mouse, document.body, '*'); + Y.delegate('mouseup', M.mod_quiz.secure_window.prevent_mouse, document.body, '*'); + Y.delegate('dragstart', M.mod_quiz.secure_window.prevent, document.body, '*'); + Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document.body, '*'); + M.mod_quiz.secure_window.clear_status; + Y.on('beforeprint', function() { + Y.one(document.body).setStyle('display', 'none'); + }, window); + Y.on('afterprint', function() { + Y.one(document.body).setStyle('display', 'block'); + }, window); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+ctrl'); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+ctrl'); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+ctrl'); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+meta'); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+meta'); + Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+meta'); + }, + + clear_status: function() { + window.status = ''; + setTimeout(M.mod_quiz.secure_window.clear_status, 10); + }, + + prevent: function(e) { + alert(M.str.quiz.functiondisabledbysecuremode); + e.halt(); + }, + + prevent_mouse: function(e) { + if (e.button != 1 || !/^(INPUT|TEXTAREA|BUTTON|SELECT)$/i.test(e.target.get('tagName'))) { + alert(M.str.quiz.functiondisabledbysecuremode); + } + e.halt(); + }, + + close: function(url, delay) { + setTimeout(function() { + if (window.opener) { + window.opener.document.location.reload(); + window.close(); + } else { + window.location.href = url; + } + }, delay*1000); + } +}; diff --git a/mod/quiz/quiz.js b/mod/quiz/quiz.js deleted file mode 100644 index 3473abb68cc..00000000000 --- a/mod/quiz/quiz.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * JavaScript library for the quiz module. - * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - */ - -function init_quiz_form() { - var responseform = document.getElementById('responseform'); - responseform.setAttribute('autocomplete', 'off'); - YAHOO.util.Event.addListener(responseform, 'keypress', check_enter); - YAHOO.util.Event.addListener(responseform, 'submit', quiz_timer.stop); -} - -/* Use this in an onkeypress handler, to stop enter submitting the forum unless you -are actually on the submit button. Don't stop the user typing things in text areas. */ -function check_enter(e) { - var target = e.target ? e.target : e.srcElement; - var keyCode = e.keyCode ? e.keyCode : e.which; - if (keyCode==13 && target.nodeName.toLowerCase()!='a' && - (!target.type || !(target.type=='submit' || target.type=='textarea'))) { - YAHOO.util.Event.preventDefault(e); - } -} - -// Code for updating the countdown timer that is used on timed quizzes. -quiz_timer = { - // The outer div, so we can get at it to move it when the page scrolls. - timerouter: null, - - // The element that the time should be displayed in. - timerdisplay: null, - - // The main quiz for, which we will need to submit when the time expires. - quizform: null, - - // String that is displayed after the time has run out. - strtimeup: '', - - // How long is left, in seconds. - endtime: 0, - - // How often we update the clock display. Delay in milliseconds. - updatedelay: 500, - - // This records the id of the timeout that updates the clock periodically, so we can cancel it - // Once time has run out. - timeoutid: null, - - // Colours used to change the timer bacground colour when time had nearly run out. - // This array is indexed by number of seconds left. - finalcolours: [ - '#ff0000', - '#ff1111', - '#ff2222', - '#ff3333', - '#ff4444', - '#ff5555', - '#ff6666', - '#ff7777', - '#ff8888', - '#ff9999', - '#ffaaaa', - '#ffbbbb', - '#ffcccc', - '#ffdddd', - '#ffeeee', - '#ffffff', - ], - - // Initialise method. - initialise: function(strtimeup, timeleft) { - // Set some fields. - quiz_timer.strtimeup = strtimeup; - quiz_timer.endtime = new Date().getTime() + timeleft*1000; - - // Get references to some bits of the DOM we need. - quiz_timer.timerouter = document.getElementById('quiz-timer'); - quiz_timer.timerdisplay = document.getElementById('quiz-time-left'); - quiz_timer.quizform = document.getElementById('responseform'); - - // Make the timer visible. - quiz_timer.timerouter.style.display = 'block'; - - // Get things started. - quiz_timer.update_time(); - }, - - // Stop method. Stops the timer if it is running. - stop: function() { - if (quiz_timer.timeoutid) { - clearTimeout(quiz_timer.timeoutid); - } - }, - - // Function that updates the text displayed in element timer_display. - set_displayed_time: function(str) { - var display = quiz_timer.timerdisplay - if (!display.firstChild) { - display.appendChild(document.createTextNode(str)) - } else if (display.firstChild.nodeType == 3) { - display.firstChild.replaceData(0, display.firstChild.length, str); - } else { - display.replaceChild(document.createTextNode(str), display.firstChild); - } - }, - - // Function to convert a number between 0 and 99 to a two-digit string. - two_digit: function(num) { - if (num < 10) { - return '0' + num; - } else { - return num; - } - }, - - // Function to update the clock with the current time left, and submit the quiz if necessary. - update_time: function() { - var secondsleft = Math.floor((quiz_timer.endtime - new Date().getTime())/1000); - - // If time has expired, Set the hidden form field that says time has expired. - if (secondsleft < 0) { - quiz_timer.stop(); - quiz_timer.set_displayed_time(quiz_timer.strtimeup); - quiz_timer.quizform.elements.timeup.value = 1; - if (quiz_timer.quizform.onsubmit) { - quiz_timer.quizform.onsubmit(); - } - quiz_timer.quizform.submit(); - return; - } - - // If time has nearly expired, change the colour. - if (secondsleft < quiz_timer.finalcolours.length) { - quiz_timer.timerouter.style.backgroundColor = quiz_timer.finalcolours[secondsleft]; - } - - // Update the time display. - var hours = Math.floor(secondsleft/3600); - secondsleft -= hours*3600; - var minutes = Math.floor(secondsleft/60); - secondsleft -= minutes*60; - var seconds = secondsleft; - quiz_timer.set_displayed_time('' + hours + ':' + quiz_timer.two_digit(minutes) + ':' + - quiz_timer.two_digit(seconds)); - - // Arrange for this method to be called again soon. - quiz_timer.timeoutid = setTimeout(quiz_timer.update_time, quiz_timer.updatedelay); - } -}; - -// Set up synchronisation between question flags and the corresponding button in the nav panel. -function quiz_init_nav_flags() { - var navblock = document.getElementById('quiznavigation'); - var buttons = YAHOO.util.Dom.getElementsByClassName('qnbutton', 'a', navblock); - for (var i = 0; i < buttons.length; i++) { - var button = buttons[i]; - var questionid = button.id.match(/\d+/)[0]; - button.stateupdater = new quiz_nav_updater(button, questionid); - } -} - -// Make the links in the attempt nav panel submit the form. -function quiz_init_attempt_nav() { - var warning = document.getElementById('quiznojswarning'); - warning.parentNode.removeChild(warning); - var navblock = document.getElementById('quiznavigation'); - var buttons = YAHOO.util.Dom.getElementsByClassName('qnbutton', 'a', navblock); - for (var i = 0; i < buttons.length; i++) { - var button = buttons[i]; - if (YAHOO.util.Dom.hasClass(button, 'thispage')) { - continue; - } - var pageidmatch = button.href.match(/page=(\d+)/); - var page; - if (pageidmatch) { - page = pageidmatch[1]; - } else { - page = 0; - } - var nav = {pageid: page}; - var questionidmatch = button.href.match(/#q(\d+)/); - if (questionidmatch) { - nav.questionid = questionidmatch[1]; - } - YAHOO.util.Event.addListener(button, 'click', quiz_nav_button_click, nav); - } - var endlink = YAHOO.util.Dom.getElementsByClassName('endtestlink', 'a', navblock)[0]; - YAHOO.util.Event.addListener(endlink, 'click', quiz_end_test_click); -} - -function quiz_nav_button_click(e, nav) { - YAHOO.util.Event.preventDefault(e); - document.getElementById('nextpagehiddeninput').value = nav.pageid; - var form = document.getElementById('responseform'); - if (nav.questionid) { - form.action += '#q' + nav.questionid; - } - form.submit(); -} - -function quiz_end_test_click(e) { - YAHOO.util.Event.preventDefault(e); - document.getElementById('nextpagehiddeninput').value = -1; - document.getElementById('responseform').submit(); -} - -function quiz_nav_updater(element, questionid) { - this.element = element; - question_flag_changer.add_flag_state_listener(questionid, this); -}; - -quiz_nav_updater.prototype.flag_state_changed = function(newstate) { - this.element.className = this.element.className.replace(/\s*\bflagged\b\s*/, ' '); - if (newstate) { - this.element.className += ' flagged'; - } -}; - -quiz_secure_window = { - // The message displayed when the secure window interferes with the user. - protection_message: null, - - // Used by close. The URL to redirect to, if we find we are not acutally in a pop-up window. - close_next_url: '', - - // Code for secure window. This used to be in protect_js.php. I don't understand it, - // I have just moved it for clenliness reasons. - initialise: function(strmessage) { - quiz_secure_window.protection_message = strmessage; - if (document.layers) { - document.captureEvents(Event.MOUSEDOWN); - } - document.onmousedown = quiz_secure_window.intercept_click; - document.oncontextmenu = function() {alert(quiz_secure_window.protection_message); return false;}; - }, - - // Code for secure window. This used to be in protect_js.php. I don't understand it, - // I have just moved it for clenliness reasons. - intercept_click: function(e) { - if (document.all) { - if (event.button==1) { - return false; - } - if (event.button==2) { - alert(quiz_securewindow_message); - return false; - } - } - if (document.layers) { - if (e.which > 1) { - alert(quiz_securewindow_message); - return false; - } - } - }, - - close: function(url, delay) { - if (url != '') { - quiz_secure_window.close_next_url = url; - } - if (delay > 0) { - setTimeout(function() {quiz_secure_window.close('', 0);}, delay*1000); - } else { - if (window.opener) { - window.opener.document.location.reload(); - window.close(); - } else if (quiz_secure_window.close_next_url != '') { - window.location.href = quiz_secure_window.close_next_url; - } - } - } -}; - -function reveal_start_button() { - document.getElementById('quizstartbutton').style.cssText = ''; -} \ No newline at end of file diff --git a/mod/quiz/review.php b/mod/quiz/review.php index 15901714519..55c572bb416 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -85,7 +85,10 @@ } } -/// Arrange for the navigation to be displayed. + // Initialise the JavaScript. + $headtags = $attemptobj->get_html_head_contributions($page); + + // Arrange for the navigation to be displayed. $navbc = $attemptobj->get_navigation_panel('quiz_review_nav_panel', $page, $showall); $firstregion = reset($PAGE->blocks->get_regions()); $PAGE->blocks->add_pretend_block($navbc, $firstregion); @@ -213,7 +216,7 @@ /// Form for saving flags if necessary. if ($options->flags == QUESTION_FLAGSEDITABLE) { echo '
'; + '" method="post" class="questionflagsaveform">
'; echo ''; } @@ -232,11 +235,11 @@ /// Close form if we opened it. if ($options->flags == QUESTION_FLAGSEDITABLE) { echo '
' . "\n" . - '' . "
\n" . "\n
\n"; - $PAGE->requires->js_function_call('question_flag_changer.init_flag_save_form', array('savingflagssubmit')); + $PAGE->requires->js_init_call('M.mod_quiz.init_review_form', null, false, quiz_get_js_module()); } /// Print a link to the next page. diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 66e4366b2af..f305594520b 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -10,6 +10,8 @@ #page-mod-quiz-summary #page .controls, #page-mod-quiz-review #page .controls {text-align: center;margin: 8px auto;} +body.jsenabled .questionflagcheckbox {display: none;} + #page-mod-quiz-edit div.question div.content .questiontext, #categoryquestions .questiontext {-o-text-overflow:ellipsis;text-overflow:ellipsis;position:relative;zoom:1;padding-left:0.3em;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} @@ -53,11 +55,37 @@ table#categoryquestions td, #page-mod-quiz-view .generalbox#feedback h2 {margin: 0;} #page-mod-quiz-view .generalbox#feedback h3 {text-align: left;} #page-mod-quiz-view .generalbox#feedback .overriddennotice {text-align: center;font-size: 0.7em;} +.quizstartbuttondiv.quizsecuremoderequired input { display: none; } +.jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; } /** Mod quiz summary **/ + #page-mod-quiz-summary #content {text-align: center;} #page-mod-quiz-summary .questionflag {width: 16px;height: 16px;vertical-align: middle;} -#page-mod-quiz-summary #quiz-timer {margin-top: 1em;display: none; /* Revealed by JavaScript if applicable */} +#page-mod-quiz-summary #quiz-timer {text-align: center;} +@media print { + .quiz-secure-window * { display: none !important; } +} + +/** Countdown timer. */ +#quiz-timer {display: none; margin-top: 1em;} +#quiz-time-left {font-weight: bold;} +#quiz-timer.timeleft15 {background: #ffffff;} +#quiz-timer.timeleft14 {background: #ffeeee;} +#quiz-timer.timeleft13 {background: #ffdddd;} +#quiz-timer.timeleft12 {background: #ffcccc;} +#quiz-timer.timeleft11 {background: #ffbbbb;} +#quiz-timer.timeleft10 {background: #ffaaaa;} +#quiz-timer.timeleft9 {background: #ff9999;} +#quiz-timer.timeleft8 {background: #ff8888;} +#quiz-timer.timeleft7 {background: #ff7777;} +#quiz-timer.timeleft6 {background: #ff6666;} +#quiz-timer.timeleft5 {background: #ff5555;} +#quiz-timer.timeleft4 {background: #ff4444;} +#quiz-timer.timeleft3 {background: #ff3333;} +#quiz-timer.timeleft2 {background: #ff2222;} +#quiz-timer.timeleft1 {background: #ff1111;} +#quiz-timer.timeleft0 {background: #ff0000;} /** Mod quiz review **/ #page-mod-quiz-review .pagingbar {margin: 1.5em auto;} @@ -99,10 +127,6 @@ table#categoryquestions td, #page-mod-quiz-grading table#grading td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: bottom;} /** Mod quiz attempt **/ -#page-mod-quiz-attempt #quiz-timer {display: none; /* Revealed by JavaScript if applicable */} -#page-mod-quiz-attempt #quiz-time-left {font-weight: bold;} -#page-mod-quiz-attempt #quiz-timer-outer {border-color: #dddddd;background: white;} - #categoryquestions .r1 {background: #e4e4e4;} #categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;} #categoryquestions th.modifiername .sorters, diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php index 798e8eb2075..9365a7fbe9a 100644 --- a/mod/quiz/summary.php +++ b/mod/quiz/summary.php @@ -51,7 +51,6 @@ if (empty($attemptobj->get_quiz()->showblocks)) { } /// Print the page header -$PAGE->requires->js('/mod/quiz/quiz.js'); $title = get_string('summaryofattempt', 'quiz'); if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) { $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' . diff --git a/question/flags.js b/question/flags.js new file mode 100644 index 00000000000..e220bdad96b --- /dev/null +++ b/question/flags.js @@ -0,0 +1,65 @@ +// This script, and the YUI libraries that it needs, are inluded by +// the $PAGE->requires->js calls in question_get_html_head_contributions in lib/questionlib.php. + +M.core_question_flags = { + flagattributes: null, + actionurl: null, + listeners: [], + + init: function(Y, actionurl, flagattributes) { + M.core_question_flags.flagattributes = flagattributes; + M.core_question_flags.actionurl = actionurl; + + Y.all('div.questionflag').each(function(flagdiv, i) { + var checkbox = flagdiv.one('input.questionflagcheckbox'); + if (!checkbox) { + return; + } + + var input = Y.Node.create(''); + input.set('id', checkbox.get('id')); + input.set('name', checkbox.get('name')); + input.set('value', checkbox.get('checked') ? 1 : 0); + + // Create an image input to replace the img tag. + var image = Y.Node.create(''); + M.core_question_flags.update_flag(input, image); + + checkbox.remove(); + flagdiv.one('label').remove(); + flagdiv.append(input); + flagdiv.append(image); + }); + + Y.delegate('click', function(e) { + var input = this.previous('input'); + input.set('value', 1 - input.get('value')); + M.core_question_flags.update_flag(input, this); + var postdata = this.previous('input.questionflagpostdata').get('value') + + input.get('value') + + e.halt(); + Y.io(M.core_question_flags.actionurl , {method: 'POST', 'data': postdata}); + M.core_question_flags.fire_listeners(postdata); + }, document.body, 'input.questionflagimage'); + + }, + + update_flag: function(input, image) { + image.setAttrs(M.core_question_flags.flagattributes[input.get('value')]); + }, + + add_listener: function(listener) { + M.core_question_flags.listeners.push(listener); + }, + + fire_listeners: function(postdata) { + for (var i = 0; i < M.core_question_flags.listeners.length; i++) { + M.core_question_flags.listeners[i]( + postdata.match(/\baid=(\d+)\b/)[1], + postdata.match(/\bqid=(\d+)\b/)[1], + postdata.match(/\bnewstate=(\d+)\b/)[1] + ); + } + } +}; diff --git a/question/preview.php b/question/preview.php index 9b99d5e1bba..7e6064143ba 100644 --- a/question/preview.php +++ b/question/preview.php @@ -207,7 +207,7 @@ $strpreview = get_string('preview', 'quiz').' '.format_string($questions[$id]->name); $questionlist = array($id); - get_html_head_contributions($questionlist, $questions, $states[$historylength]); + question_get_html_head_contributions($questionlist, $questions, $states[$historylength]); $PAGE->set_title($strpreview); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); diff --git a/question/qengine.js b/question/qengine.js deleted file mode 100644 index 1793e359238..00000000000 --- a/question/qengine.js +++ /dev/null @@ -1,92 +0,0 @@ -// This script, and the YUI libraries that it needs, are inluded by -// the $PAGE->requires->js calls in get_html_head_contributions in lib/questionlib.php. - -question_flag_changer = { - flag_state_listeners: new Object(), - - init_flag: function(checkboxid, postdata) { - // Create a hidden input - you can't just repurpose the old checkbox, IE - // does not cope - and put it in place of the checkbox. - var checkbox = document.getElementById(checkboxid); - var input = document.createElement('input'); - input.type = 'hidden'; - checkbox.parentNode.appendChild(input); - checkbox.parentNode.removeChild(checkbox); - input.id = checkbox.id; - input.name = checkbox.name; - input.value = checkbox.checked ? 1 : 0; - input.ajaxpostdata = postdata; - - // Create an image input to replace the img tag. - var image = document.createElement('input'); - image.type = 'image'; - image.statestore = input; - question_flag_changer.update_image(image); - input.parentNode.appendChild(image); - - // Remove the label. - var label = document.getElementById(checkboxid + 'label'); - label.parentNode.removeChild(label); - - // Add the event handler. - YAHOO.util.Event.addListener(image, 'click', this.flag_state_change); - }, - - init_flag_save_form: function(submitbuttonid) { - // With JS on, we just want to remove all visible traces of the form. - var button = document.getElementById(submitbuttonid); - button.parentNode.removeChild(button); - }, - - flag_state_change: function(e) { - var image = e.target ? e.target : e.srcElement; - var input = image.statestore; - input.value = 1 - input.value; - question_flag_changer.update_image(image); - var postdata = input.ajaxpostdata - if (input.value == 1) { - postdata += '&newstate=1' - } else { - postdata += '&newstate=0' - } - YAHOO.util.Event.preventDefault(e); - question_flag_changer.fire_state_changed(input); - YAHOO.util.Connect.asyncRequest('POST', qengine_config.actionurl, null, postdata); - }, - - update_image: function(image) { - if (image.statestore.value == 1) { - image.src = qengine_config.flagicon; - image.alt = qengine_config.flaggedalt; - image.title = qengine_config.unflagtooltip; - } else { - image.src = qengine_config.unflagicon; - image.alt = qengine_config.unflaggedalt; - image.title = qengine_config.flagtooltip; - } - }, - - add_flag_state_listener: function(questionid, listener) { - var key = 'q' + questionid; - if (!question_flag_changer.flag_state_listeners.hasOwnProperty(key)) { - question_flag_changer.flag_state_listeners[key] = []; - } - question_flag_changer.flag_state_listeners[key].push(listener); - }, - - questionid_from_inputid: function(inputid) { - return inputid.replace(/resp(\d+)__flagged/, '$1'); - }, - - fire_state_changed: function(input) { - var questionid = question_flag_changer.questionid_from_inputid(input.id); - var key = 'q' + questionid; - if (!question_flag_changer.flag_state_listeners.hasOwnProperty(key)) { - return; - } - var newstate = input.value == 1; - for (var i = 0; i < question_flag_changer.flag_state_listeners[key].length; i++) { - question_flag_changer.flag_state_listeners[key][i].flag_state_changed(newstate); - } - } -}; diff --git a/question/type/questiontype.php b/question/type/questiontype.php index e1ff5ac3b96..54e44956e42 100644 --- a/question/type/questiontype.php +++ b/question/type/questiontype.php @@ -967,12 +967,15 @@ class default_questiontype { $aid = $state->attempt; $qid = $state->question; $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid); - $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" . sesskey(); + $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" . + sesskey() . '&newstate='; $flagcontent = '' . - '