numbas-elm-theme/numbas-elm-display.js
Christian Lawson-Perfect 24fa9a2d77 first commit
2025-02-09 20:17:33 +00:00

1060 lines
No EOL
36 KiB
JavaScript

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) => `<numbas-part mode="part_answer" exam="${this.part.question.exam.display_id}" question="${this.part.question.number}" part="${this.part.path}g${n}"></numbas-part>`
);
}
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<Function>}
*/
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.<number>} - 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.<number>} - 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.<number>} - 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.<boolean>} answered - Has the object been answered?
* @property {observable.<boolean>} isDirty - Has the student's answer changed?
* @property {observable.<number>} score - Number of marks awarded
* @property {observable.<number>} marks - Number of marks available
* @property {observable.<number>} credit - Proportion of available marks awarded
* @property {observable.<boolean>} doesMarking - Does the object do any marking?
* @property {observable.<boolean>} 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.<boolean>} update - Call `update(true)` when the score changes. Used to trigger animations.
* @property {observable.<Numbas.display_util.feedback_state>} state - The current state of the item, to be shown to the student.
* @property {observable.<boolean>} answered - Has the item been answered? False if the student has changed their answer since submitting.
* @property {observable.<string>} answeredString - Translated text describing how much of the item has been answered: 'unanswered', 'partially answered' or 'answered'
* @property {observable.<string>} message - Text summarising the state of the item.
* @property {observable.<string>} iconClass - CSS class for the feedback icon.
* @property {observable.<object>} iconAttr - A dictionary of attributes for the feedback icon.
*/
/** Update a score feedback box.
*
* @param {Numbas.display_util.feedbackable} obj - Object to show feedback about.
* @param {Numbas.display_util.showScoreFeedback_settings} settings
* @memberof Numbas.display
* @returns {Numbas.display_util.scoreFeedback}
*/
function showScoreFeedback(obj,settings)
{
var niceNumber = Numbas.math.niceNumber;
var scoreDisplay = '';
var newScore = Knockout.observable(false);
var answered = Knockout.computed(function() {
return obj.answered();
});
var attempted = Knockout.computed(function() {
return obj.visited!==undefined && obj.visited();
});
var showFeedbackIcon = settings.showFeedbackIcon === undefined ? settings.showAnswerState : settings.showFeedbackIcon;
var anyAnswered = Knockout.computed(function() {
if(obj.anyAnswered===undefined) {
return answered();
} else {
return obj.anyAnswered();
}
});
var partiallyAnswered = Knockout.computed(function() {
return anyAnswered() && !answered();
},this);
var revealed = Knockout.computed(function() {
return (obj.revealed() && settings.reviewShowScore) || (Numbas.is_instructor && settings.reveal_answers_for_instructor!==false);
});
var state = Knockout.computed(function() {
var score = obj.score();
var marks = obj.marks();
var credit = obj.credit();
if( obj.doesMarking() && showFeedbackIcon && (revealed() || (settings.showAnswerState && anyAnswered())) ) {
if(credit<=0) {
return 'wrong';
} else if(Numbas.math.precround(credit,10)>=1) {
return 'correct';
} else {
return 'partial';
}
}
else {
return 'none';
}
});
var messageIngredients = ko.computed(function() {
var score = obj.score();
var marks = obj.marks();
var scoreobj = {
marks: marks,
score: score,
marksString: niceNumber(marks)+' '+R('mark',{count:marks}),
scoreString: niceNumber(score)+' '+R('mark',{count:score}),
};
var messageKey;
if(marks==0) {
messageKey = 'question.score feedback.not marked';
} else if(!revealed()) {
if(settings.showActualMark) {
if(settings.showTotalMark) {
messageKey = 'question.score feedback.score total actual';
} else {
messageKey = 'question.score feedback.score actual';
}
} else if(settings.showTotalMark) {
messageKey = 'question.score feedback.score total';
} else {
var key = answered () ? 'answered' : anyAnswered() ? 'partially answered' : 'unanswered';
messageKey = 'question.score feedback.'+key;
}
} else {
messageKey = 'question.score feedback.score total actual';
}
return {key: messageKey, scoreobj: scoreobj};
});
return {
update: Knockout.computed({
read: function() {
return newScore();
},
write: function() {
newScore(true);
newScore(false);
}
}),
revealed: revealed,
state: state,
answered: answered,
answeredString: Knockout.computed(function() {
if((obj.marks()==0 && !obj.doesMarking()) || !(revealed() || settings.showActualMark || settings.showTotalMark)) {
return '';
}
var key = answered() ? 'answered' : partiallyAnswered() ? 'partially answered' : 'unanswered';
return R('question.score feedback.'+key);
},this),
attemptedString: Knockout.computed(function() {
var key = attempted() ? 'attempted' : 'unattempted';
return R('question.score feedback.'+key);
},this),
message: Knockout.computed(function() {
var ingredients = messageIngredients();
return R(ingredients.key,ingredients.scoreobj);
}),
plainMessage: Knockout.computed(function() {
var ingredients = messageIngredients();
var key = ingredients.key;
if(key=='question.score feedback.score total actual' || key=='question.score feedback.score actual') {
key += '.plain';
}
return R(key,ingredients.scoreobj);
}),
iconClass: Knockout.computed(function() {
if (!showFeedbackIcon) {
return 'invisible';
}
switch(state()) {
case 'wrong':
return 'icon-remove';
case 'correct':
return 'icon-ok';
case 'partial':
return 'icon-ok partial';
default:
return '';
}
}),
iconAttr: Knockout.computed(function() {
return {title:state()=='none' ? '' : R('question.score feedback.'+state())};
})
}
};
function passwordHandler(settings) {
var value = Knockout.observable('');
var valid = Knockout.computed(function() {
return settings.accept(value());
});
return {
value: value,
valid: valid,
feedback: Knockout.computed(function() {
if(valid()) {
return {iconClass: 'icon-ok', title: settings.correct_message, buttonClass: 'btn-success'};
} else if(value()=='') {
return {iconClass: '', title: '', buttonClass: 'btn-primary'}
} else {
return {iconClass: 'icon-remove', title: settings.incorrect_message, buttonClass: 'btn-danger'};
}
})
};
}
/** Localise strings in page HTML - for tags with an attribute `data-localise`, run that attribute through R.js to localise it, and replace the tag's HTML with the result.
*/
function localisePage() {
for(let e of document.querySelectorAll('[data-localise]')) {
const localString = R(e.getAttribute('data-localise'));
e.innerHTML = localString;
}
for(let e of document.querySelectorAll('[localise-aria-label]')) {
const localString = R(e.getAttribute('localise-aria-label'));
e.setAttribute('aria-label', localString);
}
}
/** Get the attribute with the given name or, if it doesn't exist, look for localise-<name>.
* If that exists, localise its value and set the desired attribute, then return it.
*
* @param {Element} elem
* @param {string} name
* @returns {string}
*/
function getLocalisedAttribute(elem, name) {
var attr_localise;
var attr = elem.getAttribute(name);
if(!attr && (attr_localise = elem.getAttribute('localise-'+name))) {
attr = R(attr_localise);
elem.setAttribute(name,attr);
}
return attr;
}
var display_util = Numbas.display_util = {
parseRGB,
RGBToHSL,
HSLToRGB,
measureText,
showScoreFeedback,
passwordHandler,
localisePage,
getLocalisedAttribute,
};
});
/*
Copyright 2011-16 Newcastle University
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** @file Display code. Provides {@link Numbas.display} */
Numbas.queueScript('display',[],function() {
});