@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
393 lines (364 loc) • 17.2 kB
JavaScript
define(['lodash', 'i18n', 'ui/component', 'ui/itemButtonList', 'taoQtiTest/runner/helpers/map', 'handlebars', 'lib/handlebars/helpers'], function (_, __, componentFactory, itemButtonListFactory, mapHelper, Handlebars, Helpers0) { 'use strict';
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
__ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
componentFactory = componentFactory && Object.prototype.hasOwnProperty.call(componentFactory, 'default') ? componentFactory['default'] : componentFactory;
itemButtonListFactory = itemButtonListFactory && Object.prototype.hasOwnProperty.call(itemButtonListFactory, 'default') ? itemButtonListFactory['default'] : itemButtonListFactory;
mapHelper = mapHelper && Object.prototype.hasOwnProperty.call(mapHelper, 'default') ? mapHelper['default'] : mapHelper;
Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
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, options, self=this, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
function program1(depth0,data) {
return " hidden";
}
buffer += "<div class=\"qti-panel qti-navigator";
stack1 = helpers['if'].call(depth0, (depth0 && depth0.hidden), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += " qti-navigator-fizzy\">\n <div class=\"qti-navigator-header\">\n <div class=\"qti-navigator-text\">"
+ escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Test overview", options) : helperMissing.call(depth0, "__", "Test overview", options)))
+ "</div>\n <a class=\"icon-close\" href=\"#\" onclick=\"return false\" aria-hidden=\"true\"></a>\n </div>\n <nav class=\"qti-navigator-tree\"></nav>\n <div id=\"qti-navigator-linear\" class=\"qti-navigator-linear\">\n <p class=\"qti-navigator-message\">\n "
+ escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "In this part of the test navigation is not allowed.", options) : helperMissing.call(depth0, "__", "In this part of the test navigation is not allowed.", options)))
+ "\n </p>\n </div>\n</div>\n";
return buffer;
});
function navigatorTpl(data, options, asString) {
var html = Template(data, options);
return (asString || true) ? html : $(html);
}
if (!Helpers0.__initialized) {
Helpers0(Handlebars);
Helpers0.__initialized = true;
}
var Template$1 = 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, functionType="function", escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
var buffer = "", stack1;
buffer += "\n ";
stack1 = helpers.each.call(depth0, (depth0 && depth0.parts), {hash:{},inverse:self.noop,fn:self.program(2, program2, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n";
return buffer;
}
function program2(depth0,data) {
var buffer = "", stack1;
buffer += "\n ";
stack1 = helpers.each.call(depth0, (depth0 && depth0.sections), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n ";
return buffer;
}
function program3(depth0,data) {
var buffer = "", stack1, helper;
buffer += "\n <li class=\"qti-navigator-section\">\n <div class=\"qti-navigator-label\" title=\"";
if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }
else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
buffer += escapeExpression(stack1)
+ "\">\n ";
if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }
else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
buffer += escapeExpression(stack1)
+ "\n </div>\n <div class=\"qti-navigator-items\"></div>\n </li>\n ";
return buffer;
}
function program5(depth0,data) {
return "\n <li class=\"qti-navigator-section\">\n <div class=\"qti-navigator-items\"></div>\n </li>\n";
}
buffer += "<ol class=\"qti-navigator-sections plain\">\n";
stack1 = helpers['if'].call(depth0, (depth0 && depth0.displaySectionTitles), {hash:{},inverse:self.program(5, program5, data),fn:self.program(1, program1, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n</ol>\n";
return buffer;
});
function navigatorTreeTpl(data, options, asString) {
var html = Template$1(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) 2022 (original work) Open Assessment Technologies SA ;
*/
/**
* Some default values
* @type {Object}
* @private
*/
const _defaults = {
scope: 'test',
preventsUnseen: true
};
/**
* List of common CSS selectors
* @type {Object}
* @private
*/
const _selectors = {
component: '.qti-navigator',
tree: '.qti-navigator-tree',
linearState: '.qti-navigator-linear',
closeButton: '.icon-close',
itemButtonListContainer: '.qti-navigator-items'
};
/**
* @param {Object} config
* @param {String} [config.scope] Limit the review screen to a particular scope: test, testPart, testSection
* @param {Boolean} [config.preventsUnseen] Prevents the test taker to access unseen items
* @returns {*}
*/
function navigatorFactory(config) {
let component;
/**
* Handle click on an item: jump to the position, if allowed
* @param {String} itemId
*/
function onItemClick(itemId) {
const item = mapHelper.getItem(component.map, itemId);
const activeItem = mapHelper.getItem(component.map, component.testContext.itemIdentifier);
if (item && item.id && !component.is('disabled')) {
if (!(component.disableUnseenItems && !item.viewed) && (!activeItem || item.position !== activeItem.position)) {
component.select(item.position);
/**
* A jump to a particular item is required
* @event navigator#jump
* @param {Number} position - The item position on which jump
*/
component.trigger('jump', item.position);
}
}
}
/**
* Render items
* @param {Object} fizzyItemButtonMap - list of items in format needed for rendering
* @param {String} activeItemId
*/
function renderItemButtonListComponents(fizzyItemButtonMap, activeItemId) {
component.itemButtonListComponents.forEach(c => c.destroy());
component.itemButtonListComponents = [];
component.controls.$tree.find(_selectors.itemButtonListContainer).each((index, itemButtonListContainerElem) => {
const itemButtonListComponent = itemButtonListFactory({
items: fizzyItemButtonMap.sections[index].items,
scrollContainer: component.controls.$tree
}).render(itemButtonListContainerElem).on('click', _ref => {
let {
id
} = _ref;
return onItemClick(id);
});
component.itemButtonListComponents.push(itemButtonListComponent);
});
component.itemButtonListComponents.forEach(c => c.setActiveItem(activeItemId));
}
/**
* Get list of items in format needed for rendering
* @param {Object} scopedMap - test map, limited to the scope
* @param {Boolean} disableUnseenItems
* @returns {Object}
*/
function getFizzyItemButtonMap(scopedMap, disableUnseenItems) {
const {
displaySectionTitles,
displayItemTooltip
} = component.getConfig();
let nonInformationalCount = 0;
const fizzyMap = {
sections: []
};
_.forEach(scopedMap.parts, function (part) {
_.forEach(part.sections, function (dataSection) {
let fizzySection;
if (displaySectionTitles) {
fizzySection = {
label: dataSection.label,
items: []
};
fizzyMap.sections.push(fizzySection);
} else {
if (fizzyMap.sections.length) {
fizzySection = fizzyMap.sections[0];
} else {
fizzySection = {
items: []
};
fizzyMap.sections.push(fizzySection);
}
}
_.forEach(dataSection.items, function (dataItem) {
if (!dataItem.informational) {
nonInformationalCount++;
}
const fizzyItem = {
id: dataItem.id,
position: dataItem.position
};
fizzySection.items.push(fizzyItem);
fizzyItem.numericLabel = dataItem.informational ? '' : `${nonInformationalCount}`;
if (dataItem.informational) {
fizzyItem.icon = 'info';
fizzyItem.ariaLabel = __('Informational item');
} else if (dataItem.flagged) {
fizzyItem.icon = 'flagged';
fizzyItem.ariaLabel = __('Bookmarked question %s', nonInformationalCount);
} else {
fizzyItem.icon = null;
fizzyItem.ariaLabel = __('Question %s', nonInformationalCount);
}
if (dataItem.answered) {
fizzyItem.status = 'answered';
} else if (dataItem.viewed) {
fizzyItem.status = 'viewed';
} else {
fizzyItem.status = 'unseen';
}
if (disableUnseenItems && !dataItem.viewed) {
// disables all unseen items to prevent the test taker has access to.
fizzyItem.disabled = true;
} else if (displayItemTooltip) {
fizzyItem.title = dataItem.label;
}
});
});
});
return fizzyMap;
}
/**
*
* @type {Object}
*/
const navigatorApi = {
/**
* Set the marked state of an item
* @param {Number} position
* @param {Boolean} flag
*/
setItemFlag(position, flag) {
const updatedMap = _.cloneDeep(this.map);
const updatedItem = mapHelper.getItemAt(updatedMap, position);
if (updatedItem && updatedItem.id) {
updatedItem.flagged = flag;
const updatedScopeMap = mapHelper.getScopeMapFromContext(updatedMap, this.testContext, this.config.scope);
const updatedFizzyMap = getFizzyItemButtonMap(updatedScopeMap, this.disableUnseenItems);
let updatedItemData;
_.forEach(updatedFizzyMap.sections, fizzySection => {
updatedItemData = _.find(fizzySection.items, fizzyItem => fizzyItem.id === updatedItem.id);
if (updatedItemData) {
return false;
}
});
this.itemButtonListComponents.forEach(c => c.updateItem(updatedItem.id, updatedItemData));
}
},
/**
* Update the config
* @returns {navigatorApi}
*/
updateConfig() {
//not implemented
return this;
},
/**
* Updates the review screen
* @param {Object} map The current test map
* @param {Object} context The current test context
* @returns {navigatorApi}
* @fires navigator#update
*/
update(map, context) {
const scopedMap = mapHelper.getScopeMapFromContext(map, context, this.config.scope);
scopedMap.displaySectionTitles = this.getConfig().displaySectionTitles;
const activeItemId = context.itemIdentifier;
const isSkipaheadEnabled = mapHelper.hasItemCategory(map, activeItemId, 'x-tao-option-review-skipahead');
this.map = map;
this.testContext = context;
this.disableUnseenItems = this.config.preventsUnseen && !isSkipaheadEnabled;
// rebuild the tree
const testPart = mapHelper.getPart(map, context.testPartId);
if (!testPart.isLinear) {
this.setState('skipahead-enabled', isSkipaheadEnabled);
this.setState('prevents-unseen', this.config.preventsUnseen);
this.controls.$linearState.hide();
this.controls.$tree.html(navigatorTreeTpl(scopedMap));
const fizzyItemButtonMap = getFizzyItemButtonMap(scopedMap, this.disableUnseenItems);
renderItemButtonListComponents(fizzyItemButtonMap, activeItemId);
} else {
this.controls.$linearState.show();
this.controls.$tree.empty();
}
/**
* @event navigator#update the navigation data have changed
*/
this.trigger('update');
return this;
},
/**
* Selects an item
* @param {Number} position The item's position
*/
select(position) {
let previousPosition = 0;
const previousItem = mapHelper.getItem(this.map, this.testContext.itemIdentifier);
if (previousItem && previousItem.id) {
previousPosition = previousItem.position;
}
const item = mapHelper.getItemAt(this.map, parseInt(position));
if (item && item.id) {
this.itemButtonListComponents.forEach(c => c.setActiveItem(item.id));
}
/**
* An item is selected
* @param {Number} position - The item position on which select
* @param {Number} previousPosition - The item position from which select
* @event navigator#selected
*/
this.trigger('selected', position, previousPosition);
}
};
component = componentFactory(navigatorApi, _defaults).setTemplate(navigatorTpl).on('init', function () {
this.itemButtonListComponents = [];
}).on('destroy', function () {
this.controls = null;
this.itemButtonListComponents.forEach(c => c.destroy());
this.itemButtonListComponents = [];
}).on('render', function () {
const $component = this.getElement();
this.controls = {
$tree: $component.find(_selectors.tree),
$linearState: $component.find(_selectors.linearState),
$closeButton: $component.find(_selectors.closeButton)
};
this.controls.$closeButton.on('click', function (e) {
e.preventDefault();
/**
* Review screen should be closed
* @event navigator#close
*/
component.trigger('close');
});
}).on('enable', function () {
this.itemButtonListComponents.forEach(c => c.enable());
}).on('disable', function () {
this.itemButtonListComponents.forEach(c => c.disable());
});
return component.init(config);
}
return navigatorFactory;
});