Skip to content

Commit

Permalink
move display utility functions to display-util.js
Browse files Browse the repository at this point in the history
Trying to reduce the amount of duplication of code between the default
theme and the worksheet theme.

This commit moves a few utility functions to display-util.js, which
display-base imports. They're under a new namespace Numbas.display_util,
but references under Numbas.display are kept for backwards compatibility
with existing custom themes.

fixes #1060
  • Loading branch information
christianp committed Nov 16, 2023
1 parent 81fde35 commit 9ea03b6
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 635 deletions.
354 changes: 16 additions & 338 deletions themes/default/files/scripts/display-base.js
@@ -1,32 +1,9 @@
Numbas.queueScript('display-base',['controls','math','xml','util','timing','jme','jme-display'],function() {
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 */ {
/** 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.
*/
localisePage: function() {
$('[data-localise]').each(function() {
var localString = R($(this).data('localise'));
$(this).html(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}
*/
getLocalisedAttribute: function(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;
},
/** Update the progress bar when loading.
*/
showLoadProgress: function()
Expand Down Expand Up @@ -124,8 +101,8 @@ var display = Numbas.display = /** @lends Numbas.display */ {

Knockout.computed(function() {
var backgroundColour = vm.style.backgroundColour();
var rgb = parseRGB(backgroundColour);
var hsl = RGBToHSL(rgb[0],rgb[1],rgb[2]);
var rgb = display_util.parseRGB(backgroundColour);
var hsl = display_util.RGBToHSL(rgb[0],rgb[1],rgb[2]);
var oppositeBackgroundColour = hsl[2]<0.5 ? '255,255,255' : '0,0,0';
var css_vars = {
'--background-colour': vm.style.backgroundColour(),
Expand Down Expand Up @@ -379,318 +356,19 @@ var display = Numbas.display = /** @lends Numbas.display */ {
$('#die').show();
$('#die .error .message').html(message);
$('#die .error .stack').html(stack);
}
};
},

/** 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 = {};
display.measureText = function(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.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.showScoreFeedback}
*
* @typedef {object} Numbas.display.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?
*/
/** Feedback states for a question or part: "wrong", "correct", "partial" or "none".
*
* @typedef {string} Numbas.display.feedback_state
*/
/** A model representing feedback on an item which is marked - a question or a part.
*
* @typedef {object} Numbas.display.scoreFeedback
* @property {observable.<boolean>} update - Call `update(true)` when the score changes. Used to trigger animations.
* @property {observable.<Numbas.display.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.feedbackable} obj - Object to show feedback about.
* @param {Numbas.display.showScoreFeedback_settings} settings
* @memberof Numbas.display
* @returns {Numbas.display.scoreFeedback}
*/
var showScoreFeedback = display.showScoreFeedback = function(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;
});
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())};
})
}
};
// References to functions in Numbas.display_util, for backwards compatibility.
measureText: display_util.measureText,

var passwordHandler = display.passwordHandler = function(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'};
}
})
};
}
showScoreFeedback: display_util.showScoreFeedback,

passwordHandler: display_util.passwordHandler,

localisePage: display_util.localisePage,

getLocalisedAttribute: display_util.getLocalisedAttribute,

};

});

0 comments on commit 9ea03b6

Please sign in to comment.