UNPKG

@oat-sa/tao-test-runner-qti

Version:
393 lines (364 loc) 17.2 kB
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; });