Numbas.queueScript('part-display',['display-util', 'display-base','util','jme'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
var util = Numbas.util;
class PartDisplay {
constructor(p) {
this.part = p;
let parentPart = p;
while(parentPart.parentPart) {
parentPart = parentPart.parentPart;
}
this.parentPart = parentPart;
this.message_handlers = {
'part_answer': ({answer}) => this.part.storeAnswer(answer),
'submit': () => {
this.parentPart.submit();
}
};
let prompt_string = this.part.json.prompt;
if(this.part.type == 'gapfill') {
prompt_string = prompt_string.replace(
/\[\[(\d+)\]\]/g,
(m,n) => ``
);
}
this.prompt = display.makeHTMLFromString(prompt_string, this.part.getScope());
}
update_app(type, arg) {
this.part.events.trigger('update app',{type, arg, partPath: this.part.path});
}
handle_message(data) {
const {msgtype} = data;
const fn = this.message_handlers[msgtype];
if(fn) {
fn(data);
}
}
isDirty(dirty) {
this.update_app('isDirty', dirty);
}
show() {
this.update_app('show');
}
updateNextParts() {
this.update_app('updateNextParts');
}
setName(name) {
this.update_app('setName', name);
}
warning(warning) {
this.update_app('warning', warning);
}
setWarnings(warnings) {
this.update_app('setWarnings', warnings);
}
waiting_for_pre_submit(waiting) {
this.update_app('waiting_for_pre_submit', waiting);
}
showScore(answered) {
this.update_app('showScore', answered);
}
updateCorrectAnswer(answer) {
this.update_app('updateCorrectAnswer', answer);
}
showSteps() {
this.update_app('showSteps');
}
hideSteps() {
this.update_app('hideSteps');
}
revealAnswer() {
this.update_app('revealAnswer');
}
lock() {
this.update_app('lock');
}
end() {
this.update_app('end');
}
restoreAnswer() {
this.update_app('restoreAnswer');
}
}
display.PartDisplay = PartDisplay;
});
Numbas.queueScript('question-display',['display-util', 'display-base','jme-variables','xml','schedule','jme','util'],function() {
var display = Numbas.display;
class QuestionDisplay {
constructor(question) {
this.question = question;
this.message_handlers = {
'part': ({partPath, msg}) => {
const part = this.question.getPart(partPath);
part.display.handle_message(msg);
}
}
this.allParts = this.question.allParts();
}
update_app(type, arg) {
//console.log('question update app',this.question.number, {type,arg});
this.question.events.trigger('update app', {type, arg, questionNumber: this.question.number});
}
handle_message(data) {
const {msgtype} = data;
const fn = this.message_handlers[msgtype];
if(fn) {
fn(data);
}
}
isDirty(dirty) {
this.update_app('isDirty',dirty);
}
addPart(part) {
part.events.on('update app', evt => this.update_app('part', evt));
this.allParts = this.question.allParts();
}
removePart(part) {
}
resume() {
}
updateParts() {
}
end() {
}
getPart(path) {
}
init() {
}
leave() {
}
makeHTML() {
this.statement = display.makeHTMLFromString(this.question.json.statement, this.question.getScope());
this.question.signals.trigger('mainHTMLAttached');
}
revealAnswer() {
}
review() {
}
show() {
}
showAdvice() {
}
showScore() {
this.update_app('showScore');
}
}
display.QuestionDisplay = QuestionDisplay;
})
;
Numbas.queueScript('exam-display',['display-util', 'display-base','math','util','timing'],function() {
var display = Numbas.display;
var util = Numbas.util;
class ExamDisplay {
/**
* @param {Numbas.Exam} exam
*/
constructor(exam) {
this.exam = exam;
Numbas.signals.trigger('display ready');
}
update_app(type, arg) {
//console.log('exam update app', {type,arg});
this.exam.events.trigger('update app', {type, arg});
}
/** Update the timer.
*
* @memberof Numbas.display.ExamDisplay
*/
showTiming() {
//this.update_app('showTiming');
}
/** Initialise the question list display.
*
* @memberof Numbas.display.ExamDisplay
*/
initQuestionList() {
this.update_app('initQuestionList');
}
addQuestion(question) {
question.events.on('update app', (msg) => this.update_app('question',msg));
this.updateQuestionList();
}
/** Update the list of questions after a change.
*/
updateQuestionList() {
this.update_app('updateQuestionList');
}
/** Hide the timer.
*
* @memberof Numbas.display.ExamDisplay
*/
hideTiming() {
this.update_app('hideTiming');
}
/** Show/update the student's total score.
*
* @memberof Numbas.display.ExamDisplay
*/
showScore() {
this.update_app('showScore');
}
/** Show an info page (one of the front page, pause, or results).
*
* @param {string} page - Name of the page to show.
* @memberof Numbas.display.ExamDisplay
*/
showInfoPage(page) {
this.update_app('showInfoPage',page);
}
/** Show the modal dialog with actions the student can take to move on from the current question.
*/
showDiagnosticActions() {
this.update_app('showDiagnosticActions');
}
/** Show the current question.
*
* @memberof Numbas.display.ExamDisplay
*/
showQuestion() {
this.update_app('showQuestion');
}
/** Called just before the current question is regenerated.
*
* @memberof Numbas.display.ExamDisplay
*/
startRegen() {
this.update_app('startRegen');
}
/** Called after the current question has been regenerated.
*
* @memberof Numbas.display.ExamDisplay
*/
endRegen() {
this.update_app('endRegen');
}
/** Reveal the answers to every question in the exam.
*
* @memberof Numbas.display.ExamDisplay
*/
revealAnswers() {
this.update_app('revealAnswers');
}
/** Called when the exam ends.
*
* @memberof Numbas.display.ExamDisplay
*/
end() {
this.update_app('end');
}
}
display.ExamDisplay = ExamDisplay;
});
;
Numbas.queueScript('mathjax-hooks',['display-base','jme','jme-display'],function() {
/*
if(typeof MathJax=='undefined') {
return;
}
var jme = Numbas.jme;
MathJax.Hub.Register.StartupHook("TeX Jax Ready",function () {
var TEX = MathJax.InputJax.TeX;
var currentScope = null;
TEX.prefilterHooks.Add(function(data) {
currentScope = Numbas.display.find_jme_scope(data.script);
});
TEX.Definitions.Add({macros: {
'var': 'JMEvar',
'simplify': 'JMEsimplify'
}});
TEX.Parse.Augment({
JMEvar: function(name) {
var settings_string = this.GetBrackets(name);
var settings = {};
if(settings_string!==undefined) {
settings_string.split(/\s*,\s* /g).forEach(function(v) {
var setting = jme.normaliseRulesetName(v.trim());
settings[setting] = true;
});
}
var expr = this.GetArgument(name);
var scope = currentScope;
try {
var v = jme.evaluate(jme.compile(expr,scope),scope);
var tex = jme.display.texify({tok: v},settings,scope);
}catch(e) {
throw(new Numbas.Error('mathjax.math processing error',{message:e.message,expression:expr}));
}
var mml = TEX.Parse(tex,this.stack.env).mml();
this.Push(mml);
},
JMEsimplify: function(name) {
var ruleset = this.GetBrackets(name);
if(ruleset === undefined) {
ruleset = 'all';
}
var expr = this.GetArgument(name);
var scope = currentScope;
var subbed_tree = Numbas.jme.display.subvars(expr, scope);
try {
var tex = Numbas.jme.display.treeToLaTeX(subbed_tree, ruleset, scope);
} catch(e) {
throw(new Numbas.Error('mathjax.math processing error',{message:e.message,expression:expr}));
}
var mml = TEX.Parse(tex,this.stack.env).mml();
this.Push(mml);
}
})
});
*/
});
Numbas.queueScript('display/parts/custom',['display-base','part-display','util','jme'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
class CustomPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.CustomPartDisplay = CustomPartDisplay;
});
Numbas.queueScript('display/parts/extension',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
class ExtensionPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.ExtensionPartDisplay = ExtensionPartDisplay;
});
Numbas.queueScript('display/parts/gapfill',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
class GapFillPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.GapFillPartDisplay = GapFillPartDisplay;
});
Numbas.queueScript('display/parts/information',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
class InformationPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.InformationPartDisplay = InformationPartDisplay;
});
;
Numbas.queueScript('display/parts/jme',['display-base','part-display','util','jme-display','jme'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
var jme = Numbas.jme;
class JMEPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.JMEPartDisplay = JMEPartDisplay;
})
Numbas.queueScript('display/parts/matrix',['display-base','part-display','util','jme','jme-display'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
var util = Numbas.util;
class MatrixEntryPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.MatrixEntryPartDisplay = MatrixEntryPartDisplay;
});
Numbas.queueScript('display/parts/multipleresponse',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
var util = Numbas.util;
class MultipleResponsePartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.MultipleResponsePartDisplay = MultipleResponsePartDisplay;
});
Numbas.queueScript('display/parts/numberentry',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
var util = Numbas.util;
class NumberEntryPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.NumberEntryPartDisplay = NumberEntryPartDisplay;
});
Numbas.queueScript('display/parts/patternmatch',['display-base','part-display','util'],function() {
var display = Numbas.display;
var extend = Numbas.util.extend;
class PatternMatchPartDisplay extends Numbas.display.PartDisplay {
}
Numbas.display.PatternMatchPartDisplay = PatternMatchPartDisplay;
});
Numbas.queueScript('display-base',['display-util', 'controls', 'math', 'xml', 'util', 'timing', 'jme', 'jme-display'],function() {
var util = Numbas.util;
var jme = Numbas.jme;
var display_util = Numbas.display_util;
/** @namespace Numbas.display */
var display = Numbas.display = /** @lends Numbas.display */ {
/** Update the progress bar when loading.
*/
showLoadProgress: function() {
},
makeHTMLFromString(html, scope, contextDescription) {
try {
const element = document.createElement('div');
element.innerHTML = html;
Numbas.xml.localise(element);
display.setJMEScope(element, scope);
if(!element.getAttribute('data-jme-context-description')) {
element.setAttribute('data-jme-context-description',contextDescription);
}
const subbed_element = Numbas.jme.variables.DOMcontentsubvars(element,scope);
Numbas.display.register_lightbox(subbed_element);
Numbas.display.typeset(subbed_element);
return subbed_element;
} catch(error) {
console.error(error);
throw(error);
var errorContextDescriptionBits = [];
var errorContextDescription;
if(error.element) {
var elem = error.element;
while(elem) {
if(elem.nodeType==1) {
var desc = Numbas.display.getLocalisedAttribute(elem,'data-jme-context-description');
if(desc) {
errorContextDescriptionBits.splice(0,0,desc);
}
}
elem = elem.parentElement;
}
errorContextDescription = errorContextDescriptionBits.join(' ');
} else {
errorContextDescription = contextDescription;
}
Numbas.schedule.halt(new Numbas.Error('display.error making html',{contextDescription: errorContextDescription, message: error.message},error));
}
},
/**
* Find the element's top ancestor node. For elements in the document, this will be the document object itself.
*
* @param {Element} element
* @returns {Node}
*/
find_root_ancestor: function(element) {
while(element.parentNode) {
element = element.parentNode;
}
return element;
},
typeset: function(element,callback) {
if(!element) {
element = document.body;
}
let elements = [element];
var tries = 0;
var delay = 10;
const all_elements = elements.slice();
/**
* Try to typeset the given elements.
* An element is typeset if it is attached to the main document, and has a parent which specifies a JME scope to use.
*
* After each attempt, if there are any elements still waiting to be typeset, there's an exponentially growing delay before trying again.
*
* Once all elements have been typeset, the callback is called.
*/
async function try_to_typeset() {
try {
elements = elements.filter(element => {
var root = display.find_root_ancestor(element);
if(root.ownerDocument !== document) {
return true;
}
var scope = display.find_jme_scope(element);
if(!scope) {
return true;
}
return false;
});
if(elements.length) {
delay *= 1.1;
setTimeout(try_to_typeset, delay);
} else {
await MathJax.typesetPromise(all_elements);
all_elements.forEach(e => {
const root = e.getRootNode();
const s = document.getElementById('MJX-SVG-styles');
if(!s) {
return;
}
let css_tag = root.querySelector('#MJX-SVG-styles');
if(!css_tag) {
css_tag = s.cloneNode(true);
root.append(css_tag);
}
css_tag.textContent = s.textContent;
})
if(callback) {
callback(elements);
}
}
} catch(e) {
if(window.MathJax===undefined && !display.failedMathJax) {
display.failedMathJax = true;
display.showAlert("Failed to load MathJax. Maths will not be typeset properly.\n\nIf you are the exam author, please check that you are connected to the internet, or modify the theme to load a local copy of MathJax. Instructions for doing this are given in the manual.");
} else {
Numbas.schedule.halt(e);
}
}
}
setTimeout(try_to_typeset, 100);
},
register_lightbox() {
},
/** Initialise the display. Called as soon as the page loads.
*/
init: function() {
},
//alert / confirm boxes
//
/** Callback functions for the modals.
*
* @type {Object}
*/
modal: {
ok: function() {},
cancel: function() {}
},
/** Show an alert dialog.
*
* @param {string} msg - message to show the user
* @param {Function} fnOK - callback when OK is clicked
*/
showAlert: function(msg,fnOK) {
},
/** Show a confirmation dialog box.
*
* @param {string} msg - message to show the user
* @param {Function} fnOK - callback if OK is clicked
* @param {Function} fnCancel - callback if cancelled
*/
showConfirm: function(msg,fnOK,fnCancel) {
},
/** Show the end exam confirmation dialog box.
*
* @param {string} msg - message to show the user
* @param {Function} fnEnd - callback to end the exam
* @param {Function} fnCancel - callback if cancelled
*/
showConfirmEndExam: function(msg,fnEnd,fnCancel) {
},
/**
* Find the JME scope that applies to this element.
* Looks for an element with a `'jme_scope'` attribute.
*
* @param {Element} element
* @returns {Numbas.jme.Scope}
*/
find_jme_scope: function(element) {
while(element && !element.jme_scope) {
if(!element.parentElement) {
const root = element.getRootNode();
if(root !== document) {
element = root.host;
}
} else {
element = element.parentElement;
}
}
return element ? element.jme_scope : null;
},
/** Associate a JME scope with the given element.
*
* @param {Element} element
* @param {Numbas.jme.Scope} scope
*/
setJMEScope: function(element, scope) {
element.classList.add('jme-scope');
element.jme_scope = scope;
},
/** The Numbas exam has failed so much it can't continue - show an error message and the error.
*
* @param {Error} e
*/
die: function(e) {
},
};
});
;
Numbas.queueScript('display-util', ['math'], function() {
/** Parse a colour in hexadecimal RGB format into separate red, green and blue components.
*
* @param {string} hex - The hex string representing the colour, in the form `#000000`.
* @returns {Array.} - An array of the form `[r,g,b]`.
*/
function parseRGB(hex) {
var r = parseInt(hex.slice(1,3));
var g = parseInt(hex.slice(3,5));
var b = parseInt(hex.slice(5,7));
return [r,g,b];
}
/** Convert a colour given in red, green, blue components to hue, saturation, lightness.
* From https://css-tricks.com/converting-color-spaces-in-javascript/.
*
* @param {number} r - The red component.
* @param {number} g - The green component.
* @param {number} b - The blue component.
* @returns {Array.} - The colour in HSL format, an array of the form `[h,s,l]`.
* */
function RGBToHSL(r,g,b) {
r /= 255;
g /= 255;
b /= 255;
var cmin = Math.min(r,g,b);
var cmax = Math.max(r,g,b);
var delta = cmax - cmin;
var h,s,l;
if (delta == 0) {
h = 0;
} else if (cmax == r) {
h = ((g - b) / delta) % 6;
} else if (cmax == g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h = (h*60) % 360;
if (h < 0) {
h += 360;
}
l = (cmax + cmin) / 2;
s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
return [h,s,l];
}
/** Convert a colour in hue, saturation, lightness format to red, green, blue.
* From https://css-tricks.com/converting-color-spaces-in-javascript/.
*
* @param {number} h - The hue component.
* @param {number} s - The saturation component.
* @param {number} l - The lightness component.
* @returns {Array.} - An array of the form `[r,g,b]`.
*/
function HSLToRGB(h,s,l) {
var c = (1 - Math.abs(2 * l - 1)) * s;
var x = c * (1 - Math.abs((h / 60) % 2 - 1));
var m = l - c/2;
var r,g,b;
if (0 <= h && h < 60) {
r = c; g = x; b = 0;
} else if (60 <= h && h < 120) {
r = x; g = c; b = 0;
} else if (120 <= h && h < 180) {
r = 0; g = c; b = x;
} else if (180 <= h && h < 240) {
r = 0; g = x; b = c;
} else if (240 <= h && h < 300) {
r = x; g = 0; b = c;
} else if (300 <= h && h < 360) {
r = c; g = 0; b = x;
}
r = (r + m) * 255;
g = (g + m) * 255;
b = (b + m) * 255;
return [r,g,b];
}
var measurer;
var measureText_cache = {};
function measureText(element) {
var styles = window.getComputedStyle(element);
if(!measurer) {
measurer = document.createElement('div');
measurer.style['position'] = 'absolute';
measurer.style['left'] = '-10000';
measurer.style['top'] = '-10000';
measurer.style['visibility'] = 'hidden';
}
var keys = ['font-size','font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
var id = element.value+';'+keys.map(function(key) { return styles[key]; }).join(';');
if(measureText_cache[id]) {
return measureText_cache[id];
}
keys.forEach(function(key) {
measurer.style[key] = styles[key];
});
measurer.textContent = element.value;
document.body.appendChild(measurer);
var box = measurer.getBoundingClientRect();
measureText_cache[id] = box;
document.body.removeChild(measurer);
return box;
}
/** An object which can produce feedback: {@link Numbas.Question} or {@link Numbas.parts.Part}.
*
* @typedef {object} Numbas.display_util.feedbackable
* @property {observable.} answered - Has the object been answered?
* @property {observable.} isDirty - Has the student's answer changed?
* @property {observable.} score - Number of marks awarded
* @property {observable.} marks - Number of marks available
* @property {observable.} credit - Proportion of available marks awarded
* @property {observable.} doesMarking - Does the object do any marking?
* @property {observable.} revealed - Have the correct answers been revealed?
* @property {boolean} plainScore - Show the score without the "Score: " prefix?
*/
/** Settings for {@link Numbas.display_util.showScoreFeedback}
*
* @typedef {object} Numbas.display_util.showScoreFeedback_settings
* @property {boolean} showTotalMark - Show the total marks available?
* @property {boolean} showActualMark - Show the student's current score?
* @property {boolean} showAnswerState - Show the correct/incorrect state after marking?
* @property {boolean} reviewShowScore - Show the score once answers have been revealed?
* @property {boolean} [reveal_answers_for_instructor=true] - When `Numbas.is_instructor` is true, always act as if the object has been revealed?
*/
/** Feedback states for a question or part: "wrong", "correct", "partial" or "none".
*
* @typedef {string} Numbas.display_util.feedback_state
*/
/** A model representing feedback on an item which is marked - a question or a part.
*
* @typedef {object} Numbas.display_util.scoreFeedback
* @property {observable.} update - Call `update(true)` when the score changes. Used to trigger animations.
* @property {observable.} state - The current state of the item, to be shown to the student.
* @property {observable.} answered - Has the item been answered? False if the student has changed their answer since submitting.
* @property {observable.} answeredString - Translated text describing how much of the item has been answered: 'unanswered', 'partially answered' or 'answered'
* @property {observable.} message - Text summarising the state of the item.
* @property {observable.} iconClass - CSS class for the feedback icon.
* @property {observable.