Skip to content
Snippets Groups Projects
Commit 6d27ed02 authored by John Beedell's avatar John Beedell
Browse files

Qtype: STACK: Upgrade YUI to AMD javascript.

parent aba0cc27
Branches
No related tags found
No related merge requests found
define(["jquery","core/ajax","core/event"],function($,ajax,coreevent){"use strict";var StackInput=function(a,b,c,d){this.input=c,this.validationDiv=d,this.name=a,this.qaid=b,this.delayTimeoutHandle=null,this.input.addEventHanders(this),this.lastValidatedValue=this.getIntputValue(),this.validationResults={}};StackInput.prototype.TYPINGDELAY=1e3,StackInput.prototype.cancelTypingDelay=function(){this.delayTimeoutHandle&&clearTimeout(this.delayTimeoutHandle),this.delayTimeoutHandle=null},StackInput.prototype.valueChanging=function(){this.cancelTypingDelay();var a=this;this.showWaiting(),this.delayTimeoutHandle=setTimeout(function(){a.valueChanged()},this.TYPINGDELAY),setTimeout(function(){a.checkNoChange()},0)},StackInput.prototype.checkNoChange=function(){this.getIntputValue()===this.lastValidatedValue&&(this.cancelTypingDelay(),this.validationDiv.removeClass("waiting"))},StackInput.prototype.valueChanged=function(){this.cancelTypingDelay(),this.showValidationResults()||this.validateInput()},StackInput.prototype.validateInput=function(){var a=this;ajax.call([{methodname:"qtype_stack_validate_input",args:{qaid:this.qaid,name:this.name,input:this.getIntputValue()},done:function(b){a.validationReceived(b)},fail:function(b){a.showValidationFailure(b)}}]),this.showLoading()},StackInput.prototype.getIntputValue=function(){return this.input.getValue()},StackInput.prototype.validationReceived=function(a){return"invalid"===a.status?void this.showValidationFailure(a):(this.validationResults[a.input]=a,void this.showValidationResults())},StackInput.prototype.extractScripts=function(a,b){for(var c,d=/<script[^>]*>([\s\S]*?)<\/script>/g;null!==(c=d.exec(a));)b.push(c[1]);return a.replace(d,"")},StackInput.prototype.showValidationResults=function(){var val=this.getIntputValue();if(!this.validationResults[val])return this.showWaiting(),!1;var results=this.validationResults[val];this.lastValidatedValue=val;var scriptcommands=[],html=this.extractScripts(results.message,scriptcommands);this.validationDiv.html(html);for(var i=0;i<scriptcommands.length;i++)eval(scriptcommands[i]);return this.removeAllClasses(),results.message||this.validationDiv.addClass("empty"),coreevent.notifyFilterContentUpdated(this.validationDiv[0]),!0},StackInput.prototype.showValidationFailure=function(a){this.lastValidatedValue="",this.validationDiv.html(a.message),this.removeAllClasses(),this.validationDiv.addClass("error")},StackInput.prototype.showLoading=function(){this.removeAllClasses(),this.validationDiv.addClass("loading")},StackInput.prototype.showWaiting=function(){this.removeAllClasses(),this.validationDiv.addClass("waiting")},StackInput.prototype.removeAllClasses=function(){this.validationDiv.removeClass("empty"),this.validationDiv.removeClass("error"),this.validationDiv.removeClass("loading"),this.validationDiv.removeClass("waiting")};var StackSimpleInput=function(a){this.input=a};StackSimpleInput.prototype.addEventHanders=function(a){this.input.on("input",null,null,a.valueChanging.bind(a))},StackSimpleInput.prototype.getValue=function(){return this.input.val().replace(/^\s+|\s+$/g,"")};var StackTextareaInput=function(a){this.textarea=a};StackTextareaInput.prototype.addEventHanders=function(a){this.textarea.on("input",null,null,a.valueChanging.bind(a))},StackTextareaInput.prototype.getValue=function(){var a=this.textarea.val().replace(/^\s+|\s+$/g,"");return a.split(/\s*[\r\n]\s*/).join("<br>")};var StackMatrixInput=function(a,b){this.container=b,this.idPrefix=a;var c=0,d=0;this.container.find("input[type=text]").each(function(b,e){var f=$(e).attr("name");if(f.slice(0,a.length+5)===a+"_sub_"){var g=f.substring(a.length+5).split("_");d=Math.max(d,parseInt(g[0],10)+1),c=Math.max(c,parseInt(g[1],10)+1)}}),this.numcol=c,this.numrow=d};StackMatrixInput.prototype.addEventHanders=function(a){this.container.delegate("input","input[type=text]",null,a.valueChanging.bind(a))},StackMatrixInput.prototype.getValue=function(){for(var a=this.numcol,b=this.numrow,c=this.idPrefix,d=new Array(b),e=0;e<b;e++)d[e]=new Array(a);return this.container.find("input[type=text]").each(function(a,b){var e=$(b).attr("name");if(e.slice(0,c.length+5)===c+"_sub_"){var f=e.substring(c.length+5).split("_");d[f[0]][f[1]]=$(b).val().replace(/^\s+|\s+$/g,"")}}),"matrix(["+d.join("],[")+"])"};var t={initInputs:function(a,b,c){for(var d=!0,e=0;e<a.length;e++){var f=a[e];d=t.initInput(f,b,c)&&d}var g=$('input[name="'+c+':sequencecheck"]').parents("div.que.stack");if(d&&g&&(g.hasClass("dfexplicitvaildate")||g.hasClass("dfcbmexplicitvaildate"))){var h=g.find(".im-controls input.submit");h.attr("id")===c+"-submit"&&h.hide()}},initInput:function(a,b,c){var d=$(document.getElementById(c+a+"_val"));if(!d)return!1;var e=$(document.getElementById(c+a));if(e.length)return"TEXTAREA"===e[0].nodeName?new StackInput(a,b,new StackTextareaInput(e),d):new StackInput(a,b,new StackSimpleInput(e),d),!0;var f=$(document.getElementById(c+a+"_container"));return!!f.length&&(new StackInput(a,b,new StackMatrixInput(c+a,f),d),!0)}};return t});
\ No newline at end of file
// 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 <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle the real-time validation of the input the student types
* into STACK questions.
*
* @package qtype_stack
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/event'], function($, ajax, coreevent) {
"use strict";
/**
* Class constructor representing an input in a Stack question.
*
* @class StackInput
* @constructor
* @param {String} name The input name, for example ans1.
* @param {Number} qaid The question-attempt id.
* @param {Object} input An object representing the input element for this input.
* @param {Object} validationDiv jQuery representation of the validation div.
*/
var StackInput = function(name, qaid, input, validationDiv) {
this.input = input;
this.validationDiv = validationDiv;
this.name = name;
this.qaid = qaid;
this.delayTimeoutHandle = null;
this.input.addEventHanders(this);
this.lastValidatedValue = this.getIntputValue();
this.validationResults = {};
};
/**
* @config TYPINGDELAY How long a pause in typing before we make an ajax validation request.
*/
StackInput.prototype.TYPINGDELAY = 1000;
/**
* Cancel any typing pause timer.
*/
StackInput.prototype.cancelTypingDelay = function() {
if (this.delayTimeoutHandle) {
clearTimeout(this.delayTimeoutHandle);
}
this.delayTimeoutHandle = null;
};
/**
* Event handler that is fired when the input contents changes. Will do a
* validation after TYPINGDELAY if nothing else happens.
*/
StackInput.prototype.valueChanging = function() {
this.cancelTypingDelay();
var self = this;
this.showWaiting();
this.delayTimeoutHandle = setTimeout(function() {
self.valueChanged();
}, this.TYPINGDELAY);
setTimeout(function() {
self.checkNoChange();
}, 0);
};
/**
* After a small delay, detect the case where the user has got the input back
* to where they started, so not validation is necessary.
*/
StackInput.prototype.checkNoChange = function() {
if (this.getIntputValue() === this.lastValidatedValue) {
this.cancelTypingDelay();
this.validationDiv.removeClass('waiting');
}
};
/**
* Event handler that is fired when the input contents should be validated immediately.
*/
StackInput.prototype.valueChanged = function() {
this.cancelTypingDelay();
if (!this.showValidationResults()) {
this.validateInput();
}
};
/**
* Make an ajax call to validate the input.
*/
StackInput.prototype.validateInput = function() {
var self = this;
ajax.call([{
methodname: 'qtype_stack_validate_input',
args: {qaid: this.qaid, name: this.name, input: this.getIntputValue()},
done: function(response) {
self.validationReceived(response);
},
fail: function(response) {
self.showValidationFailure(response);
}
}]);
this.showLoading();
};
/**
* Returns the current value of the input.
*
* @return {String}.
*/
StackInput.prototype.getIntputValue = function() {
return this.input.getValue();
};
/**
* Update the validation div to show the results of the validation.
*
* @param {String} response The data that came back from the ajax validation call.
*/
StackInput.prototype.validationReceived = function(response) {
if (response.status === 'invalid') {
this.showValidationFailure(response);
return;
}
this.validationResults[response.input] = response;
this.showValidationResults();
};
/**
* Some browsers cannot execute JavaScript just by inserting script tags.
* To avoid that problem, remove all script tags from the given content,
* and run them later.
*
* @param {String} html HTML content
* @param {String} scriptcommands An array of script tags for later use.
* @return {String} HTML with JS removed
*/
StackInput.prototype.extractScripts = function(html, scriptcommands) {
var scriptregexp = /<script[^>]*>([\s\S]*?)<\/script>/g;
var result;
while ((result = scriptregexp.exec(html)) !== null) {
scriptcommands.push(result[1]);
}
return html.replace(scriptregexp, '');
};
/**
* Update the validation div to show the results of the validation.
*/
StackInput.prototype.showValidationResults = function() {
/*eslint no-eval: "off"*/
var val = this.getIntputValue();
if (!this.validationResults[val]) {
this.showWaiting();
return false;
}
var results = this.validationResults[val];
this.lastValidatedValue = val;
var scriptcommands = [];
var html = this.extractScripts(results.message, scriptcommands);
this.validationDiv.html(html);
// Run script commands.
for (var i = 0; i < scriptcommands.length; i++) {
eval(scriptcommands[i]);
}
this.removeAllClasses();
if (!results.message) {
this.validationDiv.addClass('empty');
}
// This fires the Maths filters for content in the validation div.
coreevent.notifyFilterContentUpdated(this.validationDiv[0]);
return true;
};
/**
* Update the validation div after an ajax validation call failed.
*
* @param {String} response The data that came back from the ajax validation call.
*/
StackInput.prototype.showValidationFailure = function(response) {
this.lastValidatedValue = '';
// Reponse usually contains backtrace, debuginfo, errorcode, link, message and moreinfourl.
this.validationDiv.html(response.message);
this.removeAllClasses();
this.validationDiv.addClass('error');
};
/**
* Display the loader icon.
*/
StackInput.prototype.showLoading = function() {
this.removeAllClasses();
this.validationDiv.addClass('loading');
};
/**
* Update the validation div to show that the input contents have changed,
* so the validation results are no longer relevant.
*/
StackInput.prototype.showWaiting = function() {
this.removeAllClasses();
this.validationDiv.addClass('waiting');
};
/**
* Strip all our class names from the validation div.
*/
StackInput.prototype.removeAllClasses = function() {
this.validationDiv.removeClass('empty');
this.validationDiv.removeClass('error');
this.validationDiv.removeClass('loading');
this.validationDiv.removeClass('waiting');
};
/**
* Class constructor representing a simple input in a Stack question.
*
* @class StackSimpleInput
* @constructor
* @param {String} name The input name, for example ans1.
*/
var StackSimpleInput = function(input) {
this.input = input;
};
/**
* Add the event handler to call when the user input changes.
*
* @param {Object} validator A StackInput object
*/
StackSimpleInput.prototype.addEventHanders = function(validator) {
// The input event fires on any change in value, even if pasted in or added by speach
// recognition to dictate text. Change only fires after loosing focus.
// Should also work on mobile.
this.input.on('input', null, null, validator.valueChanging.bind(validator));
};
/**
* Get the current value of this input.
*
* @return {String}.
*/
StackSimpleInput.prototype.getValue = function() {
return this.input.val().replace(/^\s+|\s+$/g, '');
};
/**
* Class constructor representing a textarea input.
*
* @class StackTextareaInput
* @constructor
* @param {String} name The input name, for example ans1.
*/
var StackTextareaInput = function(textarea) {
this.textarea = textarea;
};
/**
* Add the event handler to call when the user input changes.
*
* @param {Object} validator A StackInput object
*/
StackTextareaInput.prototype.addEventHanders = function(validator) {
this.textarea.on('input', null, null, validator.valueChanging.bind(validator));
};
/**
* Get the current value of this input.
*
* @return {String}.
*/
StackTextareaInput.prototype.getValue = function() {
var raw = this.textarea.val().replace(/^\s+|\s+$/g, '');
return raw.split(/\s*[\r\n]\s*/).join('<br>');
};
/**
* Class constructor representing matrx inputs (one input).
*
* @class StackMatrixInput
* @constructor
* @param {String} idPrefix.
* @param {Object} container jQuery container object.
*/
var StackMatrixInput = function(idPrefix, container) {
this.container = container;
this.idPrefix = idPrefix;
var numcol = 0;
var numrow = 0;
this.container.find('input[type=text]').each(function(i, element) {
var name = $(element).attr('name');
if (name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {
return;
}
var bits = name.substring(idPrefix.length + 5).split('_');
numrow = Math.max(numrow, parseInt(bits[0], 10) + 1);
numcol = Math.max(numcol, parseInt(bits[1], 10) + 1);
});
this.numcol = numcol;
this.numrow = numrow;
};
/**
* Add the event handler to call when the user input changes.
*
* @param {Object} validator A StackInput object
*/
StackMatrixInput.prototype.addEventHanders = function(validator) {
this.container.delegate('input', 'input[type=text]', null, validator.valueChanging.bind(validator));
};
/**
* Get the current value of this input.
*
* @return {String}.
*/
StackMatrixInput.prototype.getValue = function() {
var numcol = this.numcol;
var numrow = this.numrow;
var idPrefix = this.idPrefix;
var values = new Array(numrow);
for (var i = 0; i < numrow; i++) {
values[i] = new Array(numcol);
}
this.container.find('input[type=text]').each(function(i, element) {
var name = $(element).attr('name');
if (name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {
return;
}
var bits = name.substring(idPrefix.length + 5).split('_');
values[bits[0]][bits[1]] = $(element).val().replace(/^\s+|\s+$/g, '');
});
return 'matrix([' + values.join('],[') + '])';
};
/**
* The Stack question init return object.
*
* @alias qtype_stack/input
*/
var t = {
initInputs: function(inputs, qaid, prefix) {
var allok = true;
for (var i = 0; i < inputs.length; i++) {
var name = inputs[i];
allok = t.initInput(name, qaid, prefix) && allok;
}
var outerdiv = $('input[name="' + prefix + ':sequencecheck"]').parents('div.que.stack');
if (allok && outerdiv && (outerdiv.hasClass('dfexplicitvaildate') || outerdiv.hasClass('dfcbmexplicitvaildate'))) {
// With instant validation, we don't need the Check button, so hide it.
var button = outerdiv.find('.im-controls input.submit');
if (button.attr('id') === prefix + '-submit') {
button.hide();
}
}
},
initInput: function(name, qaid, prefix) {
var valinput = $(document.getElementById(prefix + name + '_val')); // $('#' + prefix + name + '_val') does not work!
if (!valinput) {
return false;
}
// See if it is an ordinary input.
var input = $(document.getElementById(prefix + name));
if (input.length) {
if (input[0].nodeName === 'TEXTAREA') {
new StackInput(name, qaid, new StackTextareaInput(input), valinput);
} else {
// A new StackInput object is required.
new StackInput(name, qaid, new StackSimpleInput(input), valinput);
}
return true;
}
// See if it is a matrix input.
var matrix = $(document.getElementById(prefix + name + '_container'));
if (matrix.length) {
new StackInput(name, qaid, new StackMatrixInput(prefix + name, matrix), valinput);
return true;
}
// Give up.
return false;
}
};
return t;
});
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* External API for AJAX calls.
*
* @package qtype_stack
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qtype_stack;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/externallib.php');
/**
* External API for AJAX calls.
*/
class external extends \external_api {
/**
* Returns parameter types for validate_input function.
*
* @return \external_function_parameters Parameters
*/
public static function validate_input_parameters() {
return new \external_function_parameters([
'qaid' => new \external_value(PARAM_INT, 'Question attempt id'),
'name' => new \external_value(PARAM_ALPHANUMEXT, 'Input name'),
'input' => new \external_value(PARAM_RAW, 'Input value')
]);
}
/**
* Returns result type for validate_input function.
*
* @return \external_description Result type
*/
public static function validate_input_returns() {
return new \external_single_structure([
'input' => new \external_value(PARAM_RAW, 'Input value'),
'status' => new \external_value(PARAM_ALPHA, 'One of stack_input::BLANK, stack_input::VALID, ...'),
'message' => new \external_value(PARAM_RAW, 'The answer message after validation, includes svg')
]);
}
/**
* Validates STACK question type input data.
*
* @param int $qaid Question attempt id
* @param string $name Input name
* @param mixed $input Input value
* @return array Array of input value, status and message.
*/
public static function validate_input($qaid, $name, $input) {
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/type/stack/stack/options.class.php');
require_once($CFG->dirroot . '/question/type/stack/stack/input/inputbase.class.php');
$params = self::validate_parameters(
self::validate_input_parameters(),
['qaid' => $qaid, 'name' => $name, 'input' => $input]);
self::validate_context(\context_system::instance());
$dm = new \question_engine_data_mapper();
$qa = $dm->load_question_attempt($params['qaid']);
$question = $qa->get_question();
$input = $question->inputs[$name];
$state = $question->get_input_state($params['name'], $params['input'], true);
return [
'input' => $params['input'],
'status' => $state->status,
'message' => $input->render_validation($state, $qa->get_qt_field_name($params['name']))
];
}
}
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Services.
*
* @package qtype_stack
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'qtype_stack_validate_input' => [
'classname' => '\qtype_stack\external',
'methodname' => 'validate_input',
'classpath' => '',
'description' => 'Validates STACK question type input data',
'type' => 'read',
'ajax' => true,
'capabilities' => '',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile']
]
];
......@@ -85,8 +85,8 @@ class qtype_stack_renderer extends qtype_renderer {
// Initialise automatic validation, if enabled.
if ($qaid && stack_utils::get_config()->ajaxvalidation) {
$this->page->requires->yui_module('moodle-qtype_stack-input',
'M.qtype_stack.init_inputs', array($inputstovaldiate, $qaid, $qa->get_field_prefix()));
$this->page->requires->js_call_amd('qtype_stack/input', 'initInputs',
[$inputstovaldiate, $qaid, $qa->get_field_prefix()]);
}
$result = '';
......
<?php
// This file is part of Stack - http://stack.maths.ed.ac.uk/
//
// Stack 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.
//
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* Handles ajax requests to validate the input.
*/
define('AJAX_SCRIPT', true);
require_once(__DIR__ . '/../../../../../config.php');
require_once($CFG->libdir . '/questionlib.php');
require_once(__DIR__ . '/../options.class.php');
require_once(__DIR__ . '/inputbase.class.php');
$qaid = required_param('qaid', PARAM_INT);
$inputname = required_param('name', PARAM_ALPHANUMEXT);
$inputvalue = required_param('input', PARAM_RAW);
require_login();
// This should not be necessary, but the TeX filter requires it, because it uses $OUTPUT.
$PAGE->set_context(context_system::instance());
$dm = new question_engine_data_mapper();
$qa = $dm->load_question_attempt($qaid);
$question = $qa->get_question();
$input = $question->inputs[$inputname];
$state = $question->get_input_state($inputname, $inputvalue, true);
$result = array(
'input' => $inputvalue,
'status' => $state->status,
'message' => $input->render_validation($state, $qa->get_qt_field_name($inputname)),
);
header('Content-type: application/json; charset=utf-8');
echo json_encode($result);
......@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2018120500;
$plugin->version = 2018121000;
$plugin->requires = 2015111600;
$plugin->cron = 0;
$plugin->component = 'qtype_stack';
......
YUI.add('moodle-qtype_stack-input', function (Y, NAME) {
// This file is part of Stack - http://stack.maths.ed.ac.uk/
//
// Stack 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.
//
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* This YUI module handles the real-time validation of the input the student types
* into STACK questions.
*/
/**
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_input = function(name, qaid, input, validationdiv) {
this.input = input;
this.validationdiv = validationdiv;
this.name = name;
this.qaid = qaid;
this.input.add_event_handers(this);
this.lastvalidatedvalue = this.get_intput_value();
this.validationresults = {};
};
/** Configuration. How long a pause in typing before we make an ajax validation request. */
stack_input.prototype.TYPING_DELAY = 1000,
/** YUI Node for the input we are validating. */
stack_input.prototype.input = null,
/** String name of the input we are validating. */
stack_input.prototype.name = null,
stack_input.prototype.lastvalidatedvalue = '',
/** Int question_attempt id. */
stack_input.prototype.qaid = null,
/** YUI Node for the div where the validation is displayed. */
stack_input.prototype.validationdiv = null,
/** Object known inputs => corresponding validation. */
stack_input.prototype.validationresults = {},
/** The timeout handle for the typing pause timer. */
stack_input.prototype.delay_timeout_handle = null,
/**
* Cancel any typing pause timer.
*/
stack_input.prototype.cancel_typing_delay = function() {
if (this.delay_timeout_handle) {
clearTimeout(this.delay_timeout_handle);
}
this.delay_timeout_handle = null;
};
/**
* Event handler that is fired when the input contents changes. Will do a
* validation after TYPING_DELAY if nothing else happens.
* @param e event.
*/
stack_input.prototype.value_changing = function() {
this.cancel_typing_delay();
var self = this;
this.show_waiting();
this.delay_timeout_handle = setTimeout(function() {
self.value_changed(null);
}, this.TYPING_DELAY);
setTimeout(function() {
self.check_no_change();
}, 0);
};
/**
* After a small delay, detect the case where the user has got the input back
* to where they started, so not vaildation is necessary.
*/
stack_input.prototype.check_no_change = function() {
if (this.get_intput_value() === this.lastvalidatedvalue) {
this.cancel_typing_delay();
this.validationdiv.removeClass('waiting');
}
};
/**
* Event handler that is fired when the input contents should be validated immediately.
* @param e event.
*/
stack_input.prototype.value_changed = function() {
this.cancel_typing_delay();
if (!this.show_validation_results()) {
this.validate_input();
}
};
/**
* Actually make the ajax call to validate the input.
*/
stack_input.prototype.validate_input = function() {
var self = this;
Y.io(M.cfg.wwwroot + "/question/type/stack/stack/input/ajax.php",
{
data: {
qaid: this.qaid,
name: this.name,
input: this.get_intput_value()
},
on: {
success: function(id, rawresponse) {
self.validation_received(rawresponse);
},
failure: function(id, rawresponse) {
self.show_validation_failure(rawresponse);
}
}
});
this.show_loading();
};
/**
* @return String the current value of the input.
*/
stack_input.prototype.get_intput_value = function() {
return this.input.get_value();
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.validation_received = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
if (response.error) {
this.show_validation_failure(rawresponse);
return;
}
this.validationresults[response.input] = response;
this.show_validation_results();
};
/**
* Some browsers cannot execute JavaScript just by inserting script tags.
* To avoid that problem, remove all script tags from the given content,
* and run them later.
* @param
* @param html HTML content
* @return new text with JS removed
*/
stack_input.prototype.extract_scripts = function(html, scriptcommands) {
var scriptregexp = /<script[^>]*>([\s\S]*?)<\/script>/g;
var result;
while ((result = scriptregexp.exec(html)) !== null) {
scriptcommands.push(result[1]);
}
return html.replace(scriptregexp, '');
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_results = function() {
var val = this.get_intput_value();
if (!this.validationresults[val]) {
this.show_waiting();
return false;
}
var results = this.validationresults[val];
this.lastvalidatedvalue = val;
var scriptcommands = [];
var html = this.extract_scripts(results.message, scriptcommands);
this.validationdiv.setContent(html);
// Run script commands.
for (var i = 0; i < scriptcommands.length; i++) {
eval(scriptcommands[i]); // eslint-disable-line no-eval
}
this.remove_all_classes();
if (!results.message) {
this.validationdiv.addClass('empty');
}
Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(this.validationdiv))});
return true;
};
/**
* Update the validation div to show that the ajax validation call failed.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_failure = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
this.lastvalidatedvalue = '';
this.validationdiv.setContent(response.error);
this.remove_all_classes();
this.validationdiv.addClass('error');
};
/**
* Update the validation div to show that validation is happening.
*/
stack_input.prototype.show_loading = function() {
this.remove_all_classes();
this.validationdiv.addClass('loading');
};
/**
* Update the validation div to show that the input contents have changed,
* so the validation results are no longer relevant.
*/
stack_input.prototype.show_waiting = function() {
this.remove_all_classes();
this.validationdiv.addClass('waiting');
};
/**
* Strip all our class names from the validation div.
*/
stack_input.prototype.remove_all_classes = function() {
this.validationdiv.removeClass('empty');
this.validationdiv.removeClass('error');
this.validationdiv.removeClass('loading');
this.validationdiv.removeClass('waiting');
};
/**
* Constructor. Represents simple inputs (one input).
* @param input a YUI Node object for the input element for this input.
*/
var stack_simple_input = function(input) {
this.input = input;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_simple_input.prototype.add_event_handers = function(validator) {
this.input.on('valuechange', validator.value_changing, validator);
this.input.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_simple_input.prototype.get_value = function() {
return this.input.get('value').replace(/^\s+|\s+$/g, '');
};
/**
* Constructor. Represents textarea input.
* @param textarea a YUI Node object for the textarea element for this input.
*/
var stack_textarea_input = function(textarea) {
this.textarea = textarea;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_textarea_input.prototype.add_event_handers = function(validator) {
this.textarea.on('valuechange', validator.value_changing, validator);
this.textarea.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_textarea_input.prototype.get_value = function() {
var raw = this.textarea.get('value').replace(/^\s+|\s+$/g, '');
return raw.split(/\s*[\r\n]\s*/).join('<br>');
};
/**
* Constructor. Represents simple inputs (one input).
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_matrix_input = function(idprefix, container) {
this.container = container;
this.idprefix = idprefix;
this.numcol = 0;
this.numrow = 0;
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
this.numrow = Math.max(this.numrow, parseInt(bits[0], 10) + 1);
this.numcol = Math.max(this.numcol, parseInt(bits[1], 10) + 1);
}, this);
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_matrix_input.prototype.add_event_handers = function(validator) {
this.container.delegate('valuechange', validator.value_changing, 'input[type=text]', validator);
this.container.delegate('change', validator.value_changing, 'input[type=text]', validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_matrix_input.prototype.get_value = function() {
var values = new Array(this.numrow);
for (var i = 0; i < this.numrow; i++) {
values[i] = new Array(this.numcol);
}
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
values[bits[0]][bits[1]] = e.get('value').replace(/^\s+|\s+$/g, '');
}, this);
return 'matrix([' + values.join('],[') + '])';
};
// Provide an external API.
M.qtype_stack = M.qtype_stack || {};
M.qtype_stack.init_inputs = function(inputs, qaid, prefix) {
var allok = true;
for (var i = 0; i < inputs.length; i++) {
var name = inputs[i];
allok = M.qtype_stack.init_input(name, qaid, prefix) && allok;
}
var outerdiv = Y.one('input[name="' + prefix + ':sequencecheck"]').ancestor('div.que.stack');
if (allok && outerdiv && (outerdiv.hasClass('dfexplicitvaildate') || outerdiv.hasClass('dfcbmexplicitvaildate'))) {
// With instant validation, we don't need the Check button, so hide it.
var button = outerdiv.one('.im-controls input.submit');
if (button.get('id') === prefix + '-submit') {
button.hide();
}
}
};
M.qtype_stack.init_input = function(name, qaid, prefix) {
var valinput = Y.one(document.getElementById(prefix + name + '_val'));
if (!valinput) {
return false;
}
// See if it is an ordinary input.
var input = Y.one(document.getElementById(prefix + name));
if (input) {
if (input.get('tagName') === 'TEXTAREA') {
new stack_input(name, qaid, new stack_textarea_input(input), valinput);
} else {
new stack_input(name, qaid, new stack_simple_input(input), valinput);
}
return true;
}
// See if it is a matrix input.
var matrix = Y.one(document.getElementById(prefix + name + '_container'));
if (matrix) {
new stack_input(name, qaid, new stack_matrix_input(prefix + name, matrix), valinput);
return true;
}
// Give up.
return false;
};
}, '@VERSION@', {"requires": ["node", "event-valuechange", "moodle-core-event", "io", "json-parse"]});
YUI.add("moodle-qtype_stack-input",function(Y,NAME){var stack_input=function(e,t,n,r){this.input=n,this.validationdiv=r,this.name=e,this.qaid=t,this.input.add_event_handers(this),this.lastvalidatedvalue=this.get_intput_value(),this.validationresults={}};stack_input.prototype.TYPING_DELAY=1e3,stack_input.prototype.input=null,stack_input.prototype.name=null,stack_input.prototype.lastvalidatedvalue="",stack_input.prototype.qaid=null,stack_input.prototype.validationdiv=null,stack_input.prototype.validationresults={},stack_input.prototype.delay_timeout_handle=null,stack_input.prototype.cancel_typing_delay=function(){this.delay_timeout_handle&&clearTimeout(this.delay_timeout_handle),this.delay_timeout_handle=null},stack_input.prototype.value_changing=function(){this.cancel_typing_delay();var e=this;this.show_waiting(),this.delay_timeout_handle=setTimeout(function(){e.value_changed(null)},this.TYPING_DELAY),setTimeout(function(){e.check_no_change()},0)},stack_input.prototype.check_no_change=function(){this.get_intput_value()===this.lastvalidatedvalue&&(this.cancel_typing_delay(),this.validationdiv.removeClass("waiting"))},stack_input.prototype.value_changed=function(){this.cancel_typing_delay(),this.show_validation_results()||this.validate_input()},stack_input.prototype.validate_input=function(){var e=this;Y.io(M.cfg.wwwroot+"/question/type/stack/stack/input/ajax.php",{data:{qaid:this.qaid,name:this.name,input:this.get_intput_value()},on:{success:function(t,n){e.validation_received(n)},failure:function(t,n){e.show_validation_failure(n)}}}),this.show_loading()},stack_input.prototype.get_intput_value=function(){return this.input.get_value()},stack_input.prototype.validation_received=function(e){var t=Y.JSON.parse(e.responseText);if(t.error){this.show_validation_failure(e);return}this.validationresults[t.input]=t,this.show_validation_results()},stack_input.prototype.extract_scripts=function(e,t){var n=/<script[^>]*>([\s\S]*?)<\/script>/g,r;while((r=n.exec(e))!==null)t.push(r[1]);return e.replace(n,"")},stack_input.prototype.show_validation_results=function(){var val=this.get_intput_value();if(!this.validationresults[val])return this.show_waiting(),!1;var results=this.validationresults[val];this.lastvalidatedvalue=val;var scriptcommands=[],html=this.extract_scripts(results.message,scriptcommands);this.validationdiv.setContent(html);for(var i=0;i<scriptcommands.length;i++)eval(scriptcommands[i]);return this.remove_all_classes(),results.message||this.validationdiv.addClass("empty"),Y.fire(M.core.event.FILTER_CONTENT_UPDATED,{nodes:new Y.NodeList(this.validationdiv)}),!0},stack_input.prototype.show_validation_failure=function(e){var t=Y.JSON.parse(e.responseText);this.lastvalidatedvalue="",this.validationdiv.setContent(t.error),this.remove_all_classes(),this.validationdiv.addClass("error")},stack_input.prototype.show_loading=function(){this.remove_all_classes(),this.validationdiv.addClass("loading")},stack_input.prototype.show_waiting=function(){this.remove_all_classes(),this.validationdiv.addClass("waiting")},stack_input.prototype.remove_all_classes=function(){this.validationdiv.removeClass("empty"),this.validationdiv.removeClass("error"),this.validationdiv.removeClass("loading"),this.validationdiv.removeClass("waiting")};var stack_simple_input=function(e){this.input=e};stack_simple_input.prototype.add_event_handers=function(e){this.input.on("valuechange",e.value_changing,e),this.input.on("change",e.value_changing,e)},stack_simple_input.prototype.get_value=function(){return this.input.get("value").replace(/^\s+|\s+$/g,"")};var stack_textarea_input=function(e){this.textarea=e};stack_textarea_input.prototype.add_event_handers=function(e){this.textarea.on("valuechange",e.value_changing,e),this.textarea.on("change",e.value_changing,e)},stack_textarea_input.prototype.get_value=function(){var e=this.textarea.get("value").replace(/^\s+|\s+$/g,"");return e.split(/\s*[\r\n]\s*/).join("<br>")};var stack_matrix_input=function(e,t){this.container=t,this.idprefix=e,this.numcol=0,this.numrow=0,this.container.all("input[type=text]").each(function(e){var t=e.get("name");if(t.slice(0,this.idprefix.length+5)!==this.idprefix+"_sub_")return;var n=t.substring(this.idprefix.length+5).split("_");this.numrow=Math.max(this.numrow,parseInt(n[0],10)+1),this.numcol=Math.max(this.numcol,parseInt(n[1],10)+1)},this)};stack_matrix_input.prototype.add_event_handers=function(e){this.container.delegate("valuechange",e.value_changing,"input[type=text]",e),this.container.delegate("change",e.value_changing,"input[type=text]",e)},stack_matrix_input.prototype.get_value=function(){var e=new Array(this.numrow);for(var t=0;t<this.numrow;t++)e[t]=new Array(this.numcol);return this.container.all("input[type=text]").each(function(t){var n=t.get("name");if(n.slice(0,this.idprefix.length+5)!==this.idprefix+"_sub_")return;var r=n.substring(this.idprefix.length+5).split("_");e[r[0]][r[1]]=t.get("value").replace(/^\s+|\s+$/g,"")},this),"matrix(["+e.join("],[")+"])"},M.qtype_stack=M.qtype_stack||{},M.qtype_stack.init_inputs=function(e,t,n){var r=!0;for(var i=0;i<e.length;i++){var s=e[i];r=M.qtype_stack.init_input(s,t,n)&&r}var o=Y.one('input[name="'+n+':sequencecheck"]').ancestor("div.que.stack");if(r&&o&&(o.hasClass("dfexplicitvaildate")||o.hasClass("dfcbmexplicitvaildate"))){var u=o.one(".im-controls input.submit");u.get("id")===n+"-submit"&&u.hide()}},M.qtype_stack.init_input=function(e,t,n){var r=Y.one(document.getElementById(n+e+"_val"));if(!r)return!1;var i=Y.one(document.getElementById(n+e));if(i)return i.get("tagName")==="TEXTAREA"?new stack_input(e,t,new stack_textarea_input(i),r):new stack_input(e,t,new stack_simple_input(i),r),!0;var s=Y.one(document.getElementById(n+e+"_container"));return s?(new stack_input(e,t,new stack_matrix_input(n+e,s),r),!0):!1}},"@VERSION@",{requires:["node","event-valuechange","moodle-core-event","io","json-parse"]});
YUI.add('moodle-qtype_stack-input', function (Y, NAME) {
// This file is part of Stack - http://stack.maths.ed.ac.uk/
//
// Stack 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.
//
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* This YUI module handles the real-time validation of the input the student types
* into STACK questions.
*/
/**
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_input = function(name, qaid, input, validationdiv) {
this.input = input;
this.validationdiv = validationdiv;
this.name = name;
this.qaid = qaid;
this.input.add_event_handers(this);
this.lastvalidatedvalue = this.get_intput_value();
this.validationresults = {};
};
/** Configuration. How long a pause in typing before we make an ajax validation request. */
stack_input.prototype.TYPING_DELAY = 1000,
/** YUI Node for the input we are validating. */
stack_input.prototype.input = null,
/** String name of the input we are validating. */
stack_input.prototype.name = null,
stack_input.prototype.lastvalidatedvalue = '',
/** Int question_attempt id. */
stack_input.prototype.qaid = null,
/** YUI Node for the div where the validation is displayed. */
stack_input.prototype.validationdiv = null,
/** Object known inputs => corresponding validation. */
stack_input.prototype.validationresults = {},
/** The timeout handle for the typing pause timer. */
stack_input.prototype.delay_timeout_handle = null,
/**
* Cancel any typing pause timer.
*/
stack_input.prototype.cancel_typing_delay = function() {
if (this.delay_timeout_handle) {
clearTimeout(this.delay_timeout_handle);
}
this.delay_timeout_handle = null;
};
/**
* Event handler that is fired when the input contents changes. Will do a
* validation after TYPING_DELAY if nothing else happens.
* @param e event.
*/
stack_input.prototype.value_changing = function() {
this.cancel_typing_delay();
var self = this;
this.show_waiting();
this.delay_timeout_handle = setTimeout(function() {
self.value_changed(null);
}, this.TYPING_DELAY);
setTimeout(function() {
self.check_no_change();
}, 0);
};
/**
* After a small delay, detect the case where the user has got the input back
* to where they started, so not vaildation is necessary.
*/
stack_input.prototype.check_no_change = function() {
if (this.get_intput_value() === this.lastvalidatedvalue) {
this.cancel_typing_delay();
this.validationdiv.removeClass('waiting');
}
};
/**
* Event handler that is fired when the input contents should be validated immediately.
* @param e event.
*/
stack_input.prototype.value_changed = function() {
this.cancel_typing_delay();
if (!this.show_validation_results()) {
this.validate_input();
}
};
/**
* Actually make the ajax call to validate the input.
*/
stack_input.prototype.validate_input = function() {
var self = this;
Y.io(M.cfg.wwwroot + "/question/type/stack/stack/input/ajax.php",
{
data: {
qaid: this.qaid,
name: this.name,
input: this.get_intput_value()
},
on: {
success: function(id, rawresponse) {
self.validation_received(rawresponse);
},
failure: function(id, rawresponse) {
self.show_validation_failure(rawresponse);
}
}
});
this.show_loading();
};
/**
* @return String the current value of the input.
*/
stack_input.prototype.get_intput_value = function() {
return this.input.get_value();
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.validation_received = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
if (response.error) {
this.show_validation_failure(rawresponse);
return;
}
this.validationresults[response.input] = response;
this.show_validation_results();
};
/**
* Some browsers cannot execute JavaScript just by inserting script tags.
* To avoid that problem, remove all script tags from the given content,
* and run them later.
* @param
* @param html HTML content
* @return new text with JS removed
*/
stack_input.prototype.extract_scripts = function(html, scriptcommands) {
var scriptregexp = /<script[^>]*>([\s\S]*?)<\/script>/g;
var result;
while ((result = scriptregexp.exec(html)) !== null) {
scriptcommands.push(result[1]);
}
return html.replace(scriptregexp, '');
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_results = function() {
var val = this.get_intput_value();
if (!this.validationresults[val]) {
this.show_waiting();
return false;
}
var results = this.validationresults[val];
this.lastvalidatedvalue = val;
var scriptcommands = [];
var html = this.extract_scripts(results.message, scriptcommands);
this.validationdiv.setContent(html);
// Run script commands.
for (var i = 0; i < scriptcommands.length; i++) {
eval(scriptcommands[i]); // eslint-disable-line no-eval
}
this.remove_all_classes();
if (!results.message) {
this.validationdiv.addClass('empty');
}
Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(this.validationdiv))});
return true;
};
/**
* Update the validation div to show that the ajax validation call failed.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_failure = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
this.lastvalidatedvalue = '';
this.validationdiv.setContent(response.error);
this.remove_all_classes();
this.validationdiv.addClass('error');
};
/**
* Update the validation div to show that validation is happening.
*/
stack_input.prototype.show_loading = function() {
this.remove_all_classes();
this.validationdiv.addClass('loading');
};
/**
* Update the validation div to show that the input contents have changed,
* so the validation results are no longer relevant.
*/
stack_input.prototype.show_waiting = function() {
this.remove_all_classes();
this.validationdiv.addClass('waiting');
};
/**
* Strip all our class names from the validation div.
*/
stack_input.prototype.remove_all_classes = function() {
this.validationdiv.removeClass('empty');
this.validationdiv.removeClass('error');
this.validationdiv.removeClass('loading');
this.validationdiv.removeClass('waiting');
};
/**
* Constructor. Represents simple inputs (one input).
* @param input a YUI Node object for the input element for this input.
*/
var stack_simple_input = function(input) {
this.input = input;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_simple_input.prototype.add_event_handers = function(validator) {
this.input.on('valuechange', validator.value_changing, validator);
this.input.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_simple_input.prototype.get_value = function() {
return this.input.get('value').replace(/^\s+|\s+$/g, '');
};
/**
* Constructor. Represents textarea input.
* @param textarea a YUI Node object for the textarea element for this input.
*/
var stack_textarea_input = function(textarea) {
this.textarea = textarea;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_textarea_input.prototype.add_event_handers = function(validator) {
this.textarea.on('valuechange', validator.value_changing, validator);
this.textarea.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_textarea_input.prototype.get_value = function() {
var raw = this.textarea.get('value').replace(/^\s+|\s+$/g, '');
return raw.split(/\s*[\r\n]\s*/).join('<br>');
};
/**
* Constructor. Represents simple inputs (one input).
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_matrix_input = function(idprefix, container) {
this.container = container;
this.idprefix = idprefix;
this.numcol = 0;
this.numrow = 0;
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
this.numrow = Math.max(this.numrow, parseInt(bits[0], 10) + 1);
this.numcol = Math.max(this.numcol, parseInt(bits[1], 10) + 1);
}, this);
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_matrix_input.prototype.add_event_handers = function(validator) {
this.container.delegate('valuechange', validator.value_changing, 'input[type=text]', validator);
this.container.delegate('change', validator.value_changing, 'input[type=text]', validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_matrix_input.prototype.get_value = function() {
var values = new Array(this.numrow);
for (var i = 0; i < this.numrow; i++) {
values[i] = new Array(this.numcol);
}
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
values[bits[0]][bits[1]] = e.get('value').replace(/^\s+|\s+$/g, '');
}, this);
return 'matrix([' + values.join('],[') + '])';
};
// Provide an external API.
M.qtype_stack = M.qtype_stack || {};
M.qtype_stack.init_inputs = function(inputs, qaid, prefix) {
var allok = true;
for (var i = 0; i < inputs.length; i++) {
var name = inputs[i];
allok = M.qtype_stack.init_input(name, qaid, prefix) && allok;
}
var outerdiv = Y.one('input[name="' + prefix + ':sequencecheck"]').ancestor('div.que.stack');
if (allok && outerdiv && (outerdiv.hasClass('dfexplicitvaildate') || outerdiv.hasClass('dfcbmexplicitvaildate'))) {
// With instant validation, we don't need the Check button, so hide it.
var button = outerdiv.one('.im-controls input.submit');
if (button.get('id') === prefix + '-submit') {
button.hide();
}
}
};
M.qtype_stack.init_input = function(name, qaid, prefix) {
var valinput = Y.one(document.getElementById(prefix + name + '_val'));
if (!valinput) {
return false;
}
// See if it is an ordinary input.
var input = Y.one(document.getElementById(prefix + name));
if (input) {
if (input.get('tagName') === 'TEXTAREA') {
new stack_input(name, qaid, new stack_textarea_input(input), valinput);
} else {
new stack_input(name, qaid, new stack_simple_input(input), valinput);
}
return true;
}
// See if it is a matrix input.
var matrix = Y.one(document.getElementById(prefix + name + '_container'));
if (matrix) {
new stack_input(name, qaid, new stack_matrix_input(prefix + name, matrix), valinput);
return true;
}
// Give up.
return false;
};
}, '@VERSION@', {"requires": ["node", "event-valuechange", "moodle-core-event", "io", "json-parse"]});
{
"name": "moodle-qtype_stack-input",
"builds": {
"moodle-qtype_stack-input": {
"jsfiles": [
"input.js"
]
}
}
}
// This file is part of Stack - http://stack.maths.ed.ac.uk/
//
// Stack 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.
//
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* This YUI module handles the real-time validation of the input the student types
* into STACK questions.
*/
/**
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_input = function(name, qaid, input, validationdiv) {
this.input = input;
this.validationdiv = validationdiv;
this.name = name;
this.qaid = qaid;
this.input.add_event_handers(this);
this.lastvalidatedvalue = this.get_intput_value();
this.validationresults = {};
};
/** Configuration. How long a pause in typing before we make an ajax validation request. */
stack_input.prototype.TYPING_DELAY = 1000,
/** YUI Node for the input we are validating. */
stack_input.prototype.input = null,
/** String name of the input we are validating. */
stack_input.prototype.name = null,
stack_input.prototype.lastvalidatedvalue = '',
/** Int question_attempt id. */
stack_input.prototype.qaid = null,
/** YUI Node for the div where the validation is displayed. */
stack_input.prototype.validationdiv = null,
/** Object known inputs => corresponding validation. */
stack_input.prototype.validationresults = {},
/** The timeout handle for the typing pause timer. */
stack_input.prototype.delay_timeout_handle = null,
/**
* Cancel any typing pause timer.
*/
stack_input.prototype.cancel_typing_delay = function() {
if (this.delay_timeout_handle) {
clearTimeout(this.delay_timeout_handle);
}
this.delay_timeout_handle = null;
};
/**
* Event handler that is fired when the input contents changes. Will do a
* validation after TYPING_DELAY if nothing else happens.
* @param e event.
*/
stack_input.prototype.value_changing = function() {
this.cancel_typing_delay();
var self = this;
this.show_waiting();
this.delay_timeout_handle = setTimeout(function() {
self.value_changed(null);
}, this.TYPING_DELAY);
setTimeout(function() {
self.check_no_change();
}, 0);
};
/**
* After a small delay, detect the case where the user has got the input back
* to where they started, so not vaildation is necessary.
*/
stack_input.prototype.check_no_change = function() {
if (this.get_intput_value() === this.lastvalidatedvalue) {
this.cancel_typing_delay();
this.validationdiv.removeClass('waiting');
}
};
/**
* Event handler that is fired when the input contents should be validated immediately.
* @param e event.
*/
stack_input.prototype.value_changed = function() {
this.cancel_typing_delay();
if (!this.show_validation_results()) {
this.validate_input();
}
};
/**
* Actually make the ajax call to validate the input.
*/
stack_input.prototype.validate_input = function() {
var self = this;
Y.io(M.cfg.wwwroot + "/question/type/stack/stack/input/ajax.php",
{
data: {
qaid: this.qaid,
name: this.name,
input: this.get_intput_value()
},
on: {
success: function(id, rawresponse) {
self.validation_received(rawresponse);
},
failure: function(id, rawresponse) {
self.show_validation_failure(rawresponse);
}
}
});
this.show_loading();
};
/**
* @return String the current value of the input.
*/
stack_input.prototype.get_intput_value = function() {
return this.input.get_value();
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.validation_received = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
if (response.error) {
this.show_validation_failure(rawresponse);
return;
}
this.validationresults[response.input] = response;
this.show_validation_results();
};
/**
* Some browsers cannot execute JavaScript just by inserting script tags.
* To avoid that problem, remove all script tags from the given content,
* and run them later.
* @param
* @param html HTML content
* @return new text with JS removed
*/
stack_input.prototype.extract_scripts = function(html, scriptcommands) {
var scriptregexp = /<script[^>]*>([\s\S]*?)<\/script>/g;
var result;
while ((result = scriptregexp.exec(html)) !== null) {
scriptcommands.push(result[1]);
}
return html.replace(scriptregexp, '');
};
/**
* Update the validation div to show the results of the validation.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_results = function() {
var val = this.get_intput_value();
if (!this.validationresults[val]) {
this.show_waiting();
return false;
}
var results = this.validationresults[val];
this.lastvalidatedvalue = val;
var scriptcommands = [];
var html = this.extract_scripts(results.message, scriptcommands);
this.validationdiv.setContent(html);
// Run script commands.
for (var i = 0; i < scriptcommands.length; i++) {
eval(scriptcommands[i]); // eslint-disable-line no-eval
}
this.remove_all_classes();
if (!results.message) {
this.validationdiv.addClass('empty');
}
Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(this.validationdiv))});
return true;
};
/**
* Update the validation div to show that the ajax validation call failed.
* @param e the data that came back from the ajax validation call.
*/
stack_input.prototype.show_validation_failure = function(rawresponse) {
var response = Y.JSON.parse(rawresponse.responseText);
this.lastvalidatedvalue = '';
this.validationdiv.setContent(response.error);
this.remove_all_classes();
this.validationdiv.addClass('error');
};
/**
* Update the validation div to show that validation is happening.
*/
stack_input.prototype.show_loading = function() {
this.remove_all_classes();
this.validationdiv.addClass('loading');
};
/**
* Update the validation div to show that the input contents have changed,
* so the validation results are no longer relevant.
*/
stack_input.prototype.show_waiting = function() {
this.remove_all_classes();
this.validationdiv.addClass('waiting');
};
/**
* Strip all our class names from the validation div.
*/
stack_input.prototype.remove_all_classes = function() {
this.validationdiv.removeClass('empty');
this.validationdiv.removeClass('error');
this.validationdiv.removeClass('loading');
this.validationdiv.removeClass('waiting');
};
/**
* Constructor. Represents simple inputs (one input).
* @param input a YUI Node object for the input element for this input.
*/
var stack_simple_input = function(input) {
this.input = input;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_simple_input.prototype.add_event_handers = function(validator) {
this.input.on('valuechange', validator.value_changing, validator);
this.input.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_simple_input.prototype.get_value = function() {
return this.input.get('value').replace(/^\s+|\s+$/g, '');
};
/**
* Constructor. Represents textarea input.
* @param textarea a YUI Node object for the textarea element for this input.
*/
var stack_textarea_input = function(textarea) {
this.textarea = textarea;
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_textarea_input.prototype.add_event_handers = function(validator) {
this.textarea.on('valuechange', validator.value_changing, validator);
this.textarea.on('change', validator.value_changing, validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_textarea_input.prototype.get_value = function() {
var raw = this.textarea.get('value').replace(/^\s+|\s+$/g, '');
return raw.split(/\s*[\r\n]\s*/).join('<br>');
};
/**
* Constructor. Represents simple inputs (one input).
* Constructor.
* @param name the input name, for example ans1.
* @param qaid the question-attempt id.
* @param input a YUI Node object for the input element for this input.
*/
var stack_matrix_input = function(idprefix, container) {
this.container = container;
this.idprefix = idprefix;
this.numcol = 0;
this.numrow = 0;
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
this.numrow = Math.max(this.numrow, parseInt(bits[0], 10) + 1);
this.numcol = Math.max(this.numcol, parseInt(bits[1], 10) + 1);
}, this);
};
/**
* Add the event handlers to call the value when things change.
* @param stack_input validator
*/
stack_matrix_input.prototype.add_event_handers = function(validator) {
this.container.delegate('valuechange', validator.value_changing, 'input[type=text]', validator);
this.container.delegate('change', validator.value_changing, 'input[type=text]', validator);
};
/**
* Get the current value of this input.
* @return string.
*/
stack_matrix_input.prototype.get_value = function() {
var values = new Array(this.numrow);
for (var i = 0; i < this.numrow; i++) {
values[i] = new Array(this.numcol);
}
this.container.all('input[type=text]').each(function(e) {
var name = e.get('name');
if (name.slice(0, this.idprefix.length + 5) !== this.idprefix + '_sub_') {
return;
}
var bits = name.substring(this.idprefix.length + 5).split('_');
values[bits[0]][bits[1]] = e.get('value').replace(/^\s+|\s+$/g, '');
}, this);
return 'matrix([' + values.join('],[') + '])';
};
// Provide an external API.
M.qtype_stack = M.qtype_stack || {};
M.qtype_stack.init_inputs = function(inputs, qaid, prefix) {
var allok = true;
for (var i = 0; i < inputs.length; i++) {
var name = inputs[i];
allok = M.qtype_stack.init_input(name, qaid, prefix) && allok;
}
var outerdiv = Y.one('input[name="' + prefix + ':sequencecheck"]').ancestor('div.que.stack');
if (allok && outerdiv && (outerdiv.hasClass('dfexplicitvaildate') || outerdiv.hasClass('dfcbmexplicitvaildate'))) {
// With instant validation, we don't need the Check button, so hide it.
var button = outerdiv.one('.im-controls input.submit');
if (button.get('id') === prefix + '-submit') {
button.hide();
}
}
};
M.qtype_stack.init_input = function(name, qaid, prefix) {
var valinput = Y.one(document.getElementById(prefix + name + '_val'));
if (!valinput) {
return false;
}
// See if it is an ordinary input.
var input = Y.one(document.getElementById(prefix + name));
if (input) {
if (input.get('tagName') === 'TEXTAREA') {
new stack_input(name, qaid, new stack_textarea_input(input), valinput);
} else {
new stack_input(name, qaid, new stack_simple_input(input), valinput);
}
return true;
}
// See if it is a matrix input.
var matrix = Y.one(document.getElementById(prefix + name + '_container'));
if (matrix) {
new stack_input(name, qaid, new stack_matrix_input(prefix + name, matrix), valinput);
return true;
}
// Give up.
return false;
};
{
"moodle-qtype_stack-input": {
"requires": [
"node",
"event-valuechange",
"moodle-core-event",
"io",
"json-parse"
]
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment