@oat-sa/tao-item-runner-qti
Version:
TAO QTI Item Runner modules
513 lines (463 loc) • 22.1 kB
JavaScript
define(['jquery', 'lodash', 'i18n', 'handlebars', 'lib/handlebars/helpers', 'taoQtiItem/qtiCommonRenderer/helpers/Graphic', 'taoQtiItem/qtiCommonRenderer/helpers/PciResponse', 'taoQtiItem/qtiCommonRenderer/helpers/container', 'taoQtiItem/qtiCommonRenderer/helpers/instructions/instructionManager'], function ($$1, _, __, Handlebars, Helpers0, graphic, pciResponse, containerHelper, instructionMgr) { 'use strict';
$$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
__ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
graphic = graphic && Object.prototype.hasOwnProperty.call(graphic, 'default') ? graphic['default'] : graphic;
pciResponse = pciResponse && Object.prototype.hasOwnProperty.call(pciResponse, 'default') ? pciResponse['default'] : pciResponse;
containerHelper = containerHelper && Object.prototype.hasOwnProperty.call(containerHelper, 'default') ? containerHelper['default'] : containerHelper;
instructionMgr = instructionMgr && Object.prototype.hasOwnProperty.call(instructionMgr, 'default') ? instructionMgr['default'] : instructionMgr;
if (!Helpers0.__initialized) {
Helpers0(Handlebars);
Helpers0.__initialized = true;
}
var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
var buffer = "", stack1;
buffer += "id=\""
+ escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
+ "\"";
return buffer;
}
function program3(depth0,data) {
var buffer = "", stack1;
buffer += " "
+ escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
return buffer;
}
function program5(depth0,data) {
var buffer = "", stack1;
buffer += " lang=\""
+ escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
+ "\"";
return buffer;
}
function program7(depth0,data) {
var stack1, helper;
if (helper = helpers.prompt) { stack1 = helper.call(depth0, {hash:{},data:data}); }
else { helper = (depth0 && depth0.prompt); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
if(stack1 || stack1 === 0) { return stack1; }
else { return ''; }
}
buffer += "<div ";
stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += " class=\"qti-interaction qti-blockInteraction qti-graphicInteraction qti-graphicOrderInteraction clearfix";
stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\" data-serial=\"";
if (helper = helpers.serial) { stack1 = helper.call(depth0, {hash:{},data:data}); }
else { helper = (depth0 && depth0.serial); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
buffer += escapeExpression(stack1)
+ "\"";
stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang']), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += ">\n ";
stack1 = helpers['if'].call(depth0, (depth0 && depth0.prompt), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n <div class=\"instruction-container\"></div>\n <div class=\"image-editor solid\">\n <div id='graphic-paper-";
if (helper = helpers.serial) { stack1 = helper.call(depth0, {hash:{},data:data}); }
else { helper = (depth0 && depth0.serial); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
buffer += escapeExpression(stack1)
+ "' class=\"main-image-box\"></div>\n <div class=\"clearfix\"></div>\n <ul class=\"none block-listing horizontal ordinals\"></ul>\n </div>\n</div>\n";
return buffer;
});
function tpl(data, options, asString) {
var html = Template(data, options);
return (asString || true) ? html : $(html);
}
/*
* This program 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; under version 2
* of the License (non-upgradable).
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2014-2023 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT);
*
*/
/**
* Creates ALL the texts (the numbers to display in the shapes). They are created styled but hidden.
*
* @private
* @param {Raphael.Paper} paper - the interaction paper
* @param {Number} size - the number of numbers to create...
* @param {jQueryElement} $orderList - the list than contains the orderers
* @return {Array} the creates text element
*/
const _createTexts = function _createTexts(paper, size) {
const texts = [];
_.times(size, function (index) {
const number = index + 1;
const text = graphic.createText(paper, {
id: 'text-' + number,
content: number,
title: __('Remove'),
style: 'order-text',
hide: true,
disableEvents: true
});
texts.push(text);
});
return texts;
};
/**
* Render the list of numbers
* @private
* @param {Object} interaction
* @param {jQueryElement} $orderList - the list than contains the orderers
*/
const _renderOrderList = function _renderOrderList(interaction, $orderList) {
let size = _.size(interaction.getChoices());
const min = interaction.attr('minChoices');
const max = interaction.attr('maxChoices');
//calculate the number of orderer to display
if (max > 0 && max <= size) {
size = max;
} else if (min > 0 && min <= size) {
size = min;
}
//add them to the list
_.times(size, function (index) {
const position = index + 1;
const $orderer = $$1('<li class="selectable" data-number="' + position + '">' + position + '</li>');
if (index === 0) {
$orderer.addClass('active');
}
$orderList.append($orderer);
});
//create related svg texts
_createTexts(interaction.paper, size);
//bind the activation event
const $orderers = $orderList.children('li');
$orderers.click(function (e) {
e.preventDefault();
const $orderer = $$1(this);
if (!$orderer.hasClass('active') && !$orderer.hasClass('disabled')) {
$orderers.removeClass('active');
$orderer.addClass('active');
}
});
};
/**
* Show the text that match the element's number.
* We need to display it at the center of the shape.
* @private
* @param {Raphael.Paper} paper - the interaction paper
* @param {Raphael.Element} element - the element to show the text for
*/
const _showText = function _showText(paper, element) {
const bbox = element.getBBox();
//we retrieve the good text from it's id
const text = paper.getById('text-' + element.data('number'));
if (text) {
//move it to the center of the shape (using absolute transform), and than display it
const transf = 'T' + (bbox.x + bbox.width / 2) + ',' + (bbox.y + bbox.height / 2);
text.transform(transf).show().toFront();
}
};
/**
* Select a shape to position an order
* @private
* @param {Raphael.Paper} paper - the interaction paper
* @param {Raphael.element} element - the selected shape
* @param {jQueryElement} $orderList - the list than contains the orderers
*/
const _selectShape = function _selectShape(paper, element, $orderList) {
//lookup for the active number
const $active = $orderList.find('.active:first');
if ($active.length && $active.data('number') > 0) {
//associate the current number directly to the element
element.data('number', $active.data('number'));
element.active = true;
_showText(paper, element);
graphic.updateElementState(element, 'active');
//update the state of the order list
$active.toggleClass('active disabled').siblings(':not(.disabled)').first().toggleClass('active');
}
};
/**
* Hide an element text.
* @private
* @param {Raphael.Paper} paper - the interaction paper
* @param {Raphael.Element} element - the element to hide the text for
*/
const _hideText = function _hideText(paper, element) {
const text = paper.getById('text-' + element.data('number'));
if (text) {
text.hide();
}
};
/**
* Unselect a shape to free the position
* @private
* @param {Raphael.Paper} paper - the interaction paper
* @param {Raphael.element} element - the unselected shape
* @param {jQueryElement} $orderList - the list than contains the orderers
*/
const _unselectShape = function _unselectShape(paper, element, $orderList) {
const number = element.data('number');
const unsetNumbers = [number];
$orderList.children(':not(.disabled)').each(function () {
unsetNumbers.push($$1(this).data('number'));
});
const activeNumber = Math.min.apply(Math, unsetNumbers) || number;
//update element state
element.active = false;
_hideText(paper, element);
element.removeData('number');
graphic.updateElementState(element, 'basic');
//reset order list state and activate the removed number
$orderList
.children()
.removeClass('active')
.filter('[data-number=' + number + ']')
.removeClass('disabled');
// Set (min) active number
$orderList.find('li[data-number="' + activeNumber + '"]').addClass('active');
};
/**
* Render a choice inside the paper.
* Please note that the choice renderer isn't implemented separately because it relies on the Raphael paper instead of the DOM.
* @private
* @param {Object} interaction
* @param {jQueryElement} $orderList - the list than contains the orderers
* @param {Object} choice - the hotspot choice to add to the interaction
*/
const _renderChoice = function _renderChoice(interaction, $orderList, choice) {
graphic
.createElement(interaction.paper, choice.attr('shape'), choice.attr('coords'), {
id: choice.serial,
title: __('Select this area')
})
.click(function (e) {
//if tts component is loaded and click-to-speak function is activated - we should prevent this listener to go further
if ($$1(e.currentTarget).closest('.qti-item').hasClass('prevent-click-handler')) {
return;
}
if (this.active) {
_unselectShape(interaction.paper, this, $orderList);
} else {
_selectShape(interaction.paper, this, $orderList);
}
containerHelper.triggerResponseChangeEvent(interaction);
instructionMgr.validateInstructions(interaction, { choice: choice });
});
};
/**
* Get the responses from the interaction
* @private
* @param {Object} interaction
* @returns {Array} of points
*/
const _getRawResponse = function _getRawResponse(interaction) {
const response = [];
_.forEach(interaction.getChoices(), function (choice) {
const elt = interaction.paper.getById(choice.serial);
if (elt && elt.data('number')) {
response.push({
index: elt.data('number'),
id: choice.id()
});
}
});
return _(response).sortBy('index').map('id').value();
};
/**
* Init rendering, called after template injected into the DOM
* All options are listed in the QTI v2.1 information model:
* http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321
*
* @param {object} interaction
*/
const render = function render(interaction) {
return new Promise(resolve => {
const $container = containerHelper.get(interaction);
const $orderList = $$1('ul', $container);
const background = interaction.object.attributes;
$container.off('resized.qti-widget.resolve').one('resized.qti-widget.resolve', resolve);
//create the paper
interaction.paper = graphic.responsivePaper('graphic-paper-' + interaction.serial, interaction.serial, {
width: background.width,
height: background.height,
img: this.resolveUrl(background.data),
imgId: 'bg-image-' + interaction.serial,
container: $container,
responsive: $container.hasClass('responsive')
});
//create the list of number to order
_renderOrderList(interaction, $orderList);
//call render choice for each interaction's choices
_.forEach(interaction.getChoices(), _.partial(_renderChoice, interaction, $orderList));
//set up the constraints instructions
instructionMgr.minMaxChoiceInstructions(interaction, {
min: interaction.attr('minChoices'),
max: interaction.attr('maxChoices'),
getResponse: _getRawResponse,
onError: function (data) {
graphic.highlightError(data.target);
}
});
});
};
/**
* Set the response to the rendered interaction.
*
* The response format follows the IMS PCI recommendation :
* http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343
*
* Available base types are defined in the QTI v2.1 information model:
* http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321
*
* Special value: the empty object value {} resets the interaction responses
*
* @param {object} interaction
* @param {object} response
*/
const setResponse = function (interaction, response) {
let responseValues;
const $container = containerHelper.get(interaction);
const $orderList = $$1('ul', $container);
if (response && interaction.paper) {
try {
//try to unserualize tthe pci response
responseValues = pciResponse.unserialize(response, interaction);
} catch (e) {
console.error(e);
}
if (_.isArray(responseValues)) {
_.forEach(responseValues, function (responseValue, index) {
const number = index + 1;
//get the choice that match the response
const choice = _(interaction.getChoices())
.filter({ attributes: { identifier: responseValue } })
.first();
if (choice) {
const element = interaction.paper.getById(choice.serial);
if (element) {
//activate the orderer to be consistant
$orderList.children('[data-number=' + number + ']').addClass('active');
//select the related shape
_selectShape(interaction.paper, element, $orderList);
}
}
});
}
}
};
/**
* Reset the current responses of the rendered interaction.
*
* The response format follows the IMS PCI recommendation :
* http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343
*
* Available base types are defined in the QTI v2.1 information model:
* http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321
*
* Special value: the empty object value {} resets the interaction responses
*
* @param {object} interaction
* @param {object} response
*/
const resetResponse = function resetResponse(interaction) {
const $container = containerHelper.get(interaction);
const $orderList = $$1('ul', $container);
_.forEach(interaction.getChoices(), function (choice) {
const element = interaction.paper.getById(choice.serial);
if (element) {
_unselectShape(interaction.paper, element, $orderList);
}
});
$orderList.children('li').removeClass('active disabled').first().addClass('active');
};
/**
i* Return the response of the rendered interaction
*
* The response format follows the IMS PCI recommendation :
* http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343
*
* Available base types are defined in the QTI v2.1 information model:
* http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321
*
* @param {object} interaction
* @returns {object}
*/
const getResponse = function (interaction) {
return pciResponse.serialize(_getRawResponse(interaction), interaction);
};
/**
* Clean interaction destroy
* @param {Object} interaction
*/
const destroy = function destroy(interaction) {
if (interaction.paper) {
const $container = containerHelper.get(interaction);
$$1(window).off('resize.qti-widget.' + interaction.serial);
$container.off('resize.qti-widget.' + interaction.serial);
interaction.paper.clear();
instructionMgr.removeInstructions(interaction);
$$1('.main-image-box', $container).empty().removeAttr('style');
$$1('.image-editor', $container).removeAttr('style');
$$1('ul', $container).empty();
}
//remove all references to a cache container
containerHelper.reset(interaction);
};
/**
* Set the interaction state. It could be done anytime with any state.
*
* @param {Object} interaction - the interaction instance
* @param {Object} state - the interaction state
*/
const setState = function setState(interaction, state) {
if (_.isObject(state)) {
if (state.response) {
interaction.resetResponse();
interaction.setResponse(state.response);
}
}
};
/**
* Get the interaction state.
*
* @param {Object} interaction - the interaction instance
* @returns {Object} the interaction current state
*/
const getState = function getState(interaction) {
const state = {};
const response = interaction.getResponse();
if (response) {
state.response = response;
}
return state;
};
/**
* Expose the common renderer for the interaction
* @exports qtiCommonRenderer/renderers/interactions/SelectPointInteraction
*/
var GraphicOrderInteraction = {
qtiClass: 'graphicOrderInteraction',
template: tpl,
render: render,
getContainer: containerHelper.get,
setResponse: setResponse,
getResponse: getResponse,
resetResponse: resetResponse,
destroy: destroy,
setState: setState,
getState: getState
};
return GraphicOrderInteraction;
});