UNPKG

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

Version:
976 lines (873 loc) 48.4 kB
define(['jquery', 'lodash', 'ui/component', 'ui/autoscroll', 'taoQtiTest/runner/helpers/map', 'handlebars', 'lib/handlebars/helpers'], function ($$1, _, component, autoscroll, mapHelper, Handlebars, Helpers0) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; component = component && Object.prototype.hasOwnProperty.call(component, 'default') ? component['default'] : component; autoscroll = autoscroll && Object.prototype.hasOwnProperty.call(autoscroll, 'default') ? autoscroll['default'] : autoscroll; 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, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { return " hidden"; } function program3(depth0,data) { var buffer = "", helper, options; buffer += "\n <div class=\"qti-navigator-info collapsible\">\n <span class=\"qti-navigator-label\">\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Test status", options) : helperMissing.call(depth0, "__", "Test status", options))) + "</span>\n <span class=\"icon-up\"></span>\n <span class=\"icon-down\"></span>\n </span>\n <ul class=\"collapsible-panel plain\">\n <li class=\"qti-navigator-viewed\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Viewed items", options) : helperMissing.call(depth0, "__", "Viewed items", options))) + "\">\n <span class=\"qti-navigator-label\">\n <span class=\"qti-navigator-icon icon-viewed\"></span>\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Viewed", options) : helperMissing.call(depth0, "__", "Viewed", options))) + "</span>\n <span class=\"qti-navigator-counter\">-/-</span>\n </span>\n </li>\n <li class=\"qti-navigator-answered\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Completed items", options) : helperMissing.call(depth0, "__", "Completed items", options))) + "\">\n <span class=\"qti-navigator-label\">\n <span class=\"qti-navigator-icon icon-answered\"></span>\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Answered", options) : helperMissing.call(depth0, "__", "Answered", options))) + "</span>\n <span class=\"qti-navigator-counter\">-/-</span>\n </span>\n </li>\n <li class=\"qti-navigator-unanswered\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Unanswered items", options) : helperMissing.call(depth0, "__", "Unanswered items", options))) + "\">\n <span class=\"qti-navigator-label\">\n <span class=\"qti-navigator-icon icon-unanswered\"></span>\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Unanswered", options) : helperMissing.call(depth0, "__", "Unanswered", options))) + "</span>\n <span class=\"qti-navigator-counter\">-/-</span>\n </span>\n </li>\n <li class=\"qti-navigator-flagged\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Items marked for later review", options) : helperMissing.call(depth0, "__", "Items marked for later review", options))) + "\">\n <span class=\"qti-navigator-label\">\n <span class=\"qti-navigator-icon icon-flagged\"></span>\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Flagged", options) : helperMissing.call(depth0, "__", "Flagged", options))) + "</span>\n <span class=\"qti-navigator-counter\">-/-</span>\n </span>\n </li>\n </ul>\n </div>\n "; return buffer; } function program5(depth0,data) { return "(<span class=\"qti-navigator-counter\">0</span>)"; } function program7(depth0,data) { return "icon-unanswered "; } function program9(depth0,data) { var buffer = "", helper, options; buffer += escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Unanswered", options) : helperMissing.call(depth0, "__", "Unanswered", options))) + " (<span class=\"qti-navigator-counter\">0</span>)"; return buffer; } function program11(depth0,data) { return "icon-flagged "; } function program13(depth0,data) { var buffer = "", helper, options; buffer += escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Flagged", options) : helperMissing.call(depth0, "__", "Flagged", options))) + " (<span class=\"qti-navigator-counter\">0</span>)"; return buffer; } buffer += "<div class=\"qti-panel qti-navigator qti-navigator-default"; 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 += "\">\n <div class=\"qti-navigator-collapsible\">\n <span class=\"qti-navigator-collapse icon icon-left\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Collapse the review panel", options) : helperMissing.call(depth0, "__", "Collapse the review panel", options))) + "\"></span>\n <span class=\"qti-navigator-expand icon icon-right\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Expand the review panel", options) : helperMissing.call(depth0, "__", "Expand the review panel", options))) + "\"></span>\n </div>\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n\n\n <div class=\"qti-navigator-filters\">\n <ul role=\"tablist\" class=\"plain clearfix\">\n <li role=\"tab\" aria-selected=\"true\" class=\"qti-navigator-filter active\" data-mode=\"all\">\n <span title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Reset filters", options) : helperMissing.call(depth0, "__", "Reset filters", options))) + "\" class=\"qti-navigator-tab\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "All", options) : helperMissing.call(depth0, "__", "All", options))) + "\n "; stack1 = helpers.unless.call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </span>\n </li>\n\n <li role=\"tab\" class=\"qti-navigator-filter\" data-mode=\"unanswered\">\n <span class=\""; stack1 = helpers['if'].call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "qti-navigator-tab\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Only display the unanswered items", options) : helperMissing.call(depth0, "__", "Only display the unanswered items", options))) + "\">\n "; stack1 = helpers.unless.call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </span>\n </li>\n\n <li role=\"tab\" class=\"qti-navigator-filter\" data-mode=\"flagged\">\n <span class=\""; stack1 = helpers['if'].call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(11, program11, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "qti-navigator-tab\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Only display the items marked for review", options) : helperMissing.call(depth0, "__", "Only display the items marked for review", options))) + "\">\n "; stack1 = helpers.unless.call(depth0, (depth0 && depth0.showLegend), {hash:{},inverse:self.noop,fn:self.program(13, program13, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </span>\n </li>\n </ul>\n </div>\n\n <nav class=\"qti-navigator-tree\"></nav>\n\n <div id=\"qti-navigator-linear\" class=\"qti-navigator-linear\">\n <span class=\"icon icon-info\" title=\"" + 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))) + "\"></span>\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, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType="function", self=this; function program1(depth0,data) { var buffer = "", stack1, helper; buffer += "\n <li class=\"qti-navigator-part collapsible "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.active), {hash:{},inverse:self.program(4, program4, data),fn:self.program(2, program2, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\" data-id=\""; if (helper = helpers.id) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.id); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\">\n <span 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 <span class=\"qti-navigator-text\">"; 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) + "</span>\n <span class=\"icon-up\"></span>\n <span class=\"icon-down\"></span>\n </span>\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.isLinear), {hash:{},inverse:self.program(8, program8, data),fn:self.program(6, program6, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </li>\n "; return buffer; } function program2(depth0,data) { return "active"; } function program4(depth0,data) { return "collapsed"; } function program6(depth0,data) { var buffer = "", stack1, helper, options; buffer += "\n <div class=\"qti-navigator-linear-part collapsible-panel\">\n <span class=\"icon icon-info\" title=\"" + 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))) + "\"></span>\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 <p class=\"qti-navigator-actions\">\n <button class=\"btn-info small\" data-position=\""; if (helper = helpers.position) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.position); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Start Test-part", options) : helperMissing.call(depth0, "__", "Start Test-part", options))) + "\">\n <span class=\"qti-navigator-text\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Start Test-part", options) : helperMissing.call(depth0, "__", "Start Test-part", options))) + "</span>\n <span class=\"icon-play r\"></span>\n </button>\n </p>\n </div>\n "; return buffer; } function program8(depth0,data) { var buffer = "", stack1, helper; buffer += "\n <ul aria-label=\""; 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) + "\" class=\"qti-navigator-sections collapsible-panel plain\">\n "; stack1 = helpers.each.call(depth0, (depth0 && depth0.sections), {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </ul>\n "; return buffer; } function program9(depth0,data) { var buffer = "", stack1, helper; buffer += "\n <li class=\"qti-navigator-section collapsible "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.active), {hash:{},inverse:self.program(4, program4, data),fn:self.program(2, program2, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\" data-id=\""; if (helper = helpers.id) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.id); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\">\n <span 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 <span class=\"qti-navigator-text\">"; 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) + "</span>\n <span class=\"qti-navigator-counter\">" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.stats)),stack1 == null || stack1 === false ? stack1 : stack1.answered)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "/" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.stats)),stack1 == null || stack1 === false ? stack1 : stack1.total)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "</span>\n </span>\n <ul aria-label=\""; 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) + "\" class=\"qti-navigator-items collapsible-panel plain\">\n "; stack1 = helpers.each.call(depth0, (depth0 && depth0.items), {hash:{},inverse:self.noop,fn:self.programWithDepth(10, program10, data, depth0),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n </ul>\n </li>\n "; return buffer; } function program10(depth0,data,depth1) { var buffer = "", stack1, helper; buffer += "\n <li class=\"qti-navigator-item "; if (helper = helpers.cls) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.cls); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\" data-id=\""; if (helper = helpers.id) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.id); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\" data-position=\""; if (helper = helpers.position) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.position); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\">\n <span class=\"qti-navigator-label truncate\" 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 role=\"link\" aria-disabled=\""; stack1 = helpers['if'].call(depth0, (depth0 && depth0.viewed), {hash:{},inverse:self.program(13, program13, data),fn:self.program(11, program11, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.active), {hash:{},inverse:self.noop,fn:self.program(15, program15, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n aria-label=\""; if (helper = helpers.index) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.index); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + " of " + escapeExpression(((stack1 = ((stack1 = (depth1 && depth1.stats)),stack1 == null || stack1 === false ? stack1 : stack1.total)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + " "; if (helper = helpers.icon) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.icon); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\">\n <span class=\"qti-navigator-icon icon-"; if (helper = helpers.icon) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.icon); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"></span>\n <span class=\"qti-navigator-number\">"; if (helper = helpers.index) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.index); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "</span>\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 </span>\n </li>\n "; return buffer; } function program11(depth0,data) { return "false"; } function program13(depth0,data) { return "true"; } function program15(depth0,data) { return "aria-current=\"page\""; } buffer += "<ul class=\"qti-navigator-parts plain\">\n "; stack1 = helpers.each.call(depth0, (depth0 && depth0.parts), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n</ul>\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) 2016-2019 (original work) Open Assessment Technologies SA ; */ /** * Some default values * @type {Object} * @private */ var _defaults = { scope: 'test', canCollapse: false, preventsUnseen: true, hidden: false }; /** * List of CSS classes * @type {Object} * @private */ var _cssCls = { active: 'active', collapsed: 'collapsed', collapsible: 'collapsible', hidden: 'hidden', disabled: 'disabled', flagged: 'flagged', answered: 'answered', viewed: 'viewed', unseen: 'unseen', info: 'info', icon: 'qti-navigator-icon', scope: { test: 'scope-test', testPart: 'scope-test-part', testSection: 'scope-test-section' } }; /** * List of icon CSS classes * @type {Array} * @private */ var _iconCls = [_cssCls.info, _cssCls.flagged, _cssCls.answered, _cssCls.viewed]; /** * List of common CSS selectors * @type {Object} * @private */ var _selectors = { component: '.qti-navigator', filterBar: '.qti-navigator-filters', filter: '.qti-navigator-filter', tree: '.qti-navigator-tree', collapseHandle: '.qti-navigator-collapsible', linearState: '.qti-navigator-linear', infoAnswered: '.qti-navigator-answered .qti-navigator-counter', infoViewed: '.qti-navigator-viewed .qti-navigator-counter', infoUnanswered: '.qti-navigator-unanswered .qti-navigator-counter', infoFlagged: '.qti-navigator-flagged .qti-navigator-counter', infoPanel: '.qti-navigator-info', infoPanelLabels: '.qti-navigator-info > .qti-navigator-label', tabInfoAll: '[data-mode="all"] .qti-navigator-counter', tabInfoUnanswered: '[data-mode="unanswered"] .qti-navigator-counter', tabInfoFlagged: '[data-mode="flagged"] .qti-navigator-counter', parts: '.qti-navigator-part', partLabels: '.qti-navigator-part > .qti-navigator-label', sections: '.qti-navigator-section', sectionLabels: '.qti-navigator-section > .qti-navigator-label', items: '.qti-navigator-item', itemLabels: '.qti-navigator-item > .qti-navigator-label', itemIcons: '.qti-navigator-item > .qti-navigator-icon', activeItem: '.qti-navigator-item.active', icons: '.qti-navigator-icon', linearStart: '.qti-navigator-linear-part button', counters: '.qti-navigator-counter', actives: '.active', collapsible: '.collapsible', collapsiblePanels: '.collapsible-panel', unseen: '.unseen', answered: '.answered', flagged: '.flagged', notFlagged: ':not(.flagged)', notAnswered: ':not(.answered)', notInformational: ':not(.info)', informational: '.info', hidden: '.hidden', disabled: '.disabled' }; /** * Maps the filter mode to filter criteria. * Each filter criteria is a CSS selector used to find and mask the items to be discarded by the filter. * @type {Object} * @private */ var _filterMap = { all: '', unanswered: [_selectors.answered, _selectors.informational].join(','), flagged: _selectors.notFlagged, answered: _selectors.notAnswered, filtered: _selectors.hidden }; /** * * @type {Object} */ var navigatorApi = { /** * Updates the stats on the flagged items in the current map * @param {Number} position * @param {Boolean} flag */ updateStats: function updateStats(position, flag) { var map = this.map; var item; if (map) { item = mapHelper.getItemAt(map, position); if (item) { item.flagged = flag; mapHelper.updateItemStats(map, position); } } }, /** * Gets the total number of items for the provided target * @param {Object} progression * @param {String} target * @returns {Number} */ getProgressionTotal: function getProgressionTotal(progression, target) { var total; if ('questions' === target) { total = progression.questions; } else { total = progression.total; } return total; }, /** * Set the marked state of an item * @param {Number|String|jQuery} position * @param {Boolean} flag */ setItemFlag: function setItemFlag(position, flag) { var $item = position && position.jquery ? position : this.controls.$tree.find(`[data-position=${position}]`); var progression = this.progression; var icon; // update the map stats this.updateStats(position, flag); // update the item flag $item.toggleClass(_cssCls.flagged, flag); // set the item icon according to its state icon = _.find(_iconCls, _.bind($item.hasClass, $item)) || _cssCls.unseen; $item.find(_selectors.icons).attr('class', `${_cssCls.icon} icon-${icon}`); // update the info panel progression.flagged = this.controls.$tree.find(_selectors.flagged).length; this.writeCount(this.controls.$infoFlagged, progression.flagged, this.getProgressionTotal(progression, 'questions')); // recompute the filters this.filter(this.currentFilter); }, /** * Filters the items by a criteria * @param {String} criteria */ filter: function filter(criteria) { var self = this; // remove the current filter by restoring all items var $items = this.controls.$tree.find(_selectors.items).removeClass(_cssCls.hidden); // filter the items according to the provided criteria var filterCb = _filterMap[criteria]; if (filterCb) { $items.filter(filterCb).addClass(_cssCls.hidden); } // update the section counters this.controls.$tree.find(_selectors.sections).each(function () { var $section = $$1(this); var $itemsFound = $section.find(_selectors.items).not(_selectors.hidden); var $filtered = $itemsFound.not(_selectors.disabled); self.writeCount($section.find(_selectors.counters), $filtered.length, $itemsFound.length); }); this.currentFilter = criteria; }, /** * Update the config * @param {Object} [config] * @returns {navigatorApi} */ updateConfig: function updateConfig(config) { var $component = this.getElement(); var scopeClass = _cssCls.scope[this.config.scope || _defaults.scope]; // apply the new config config = _.merge(this.config, config || {}); // enable/disable the collapsing of the panel $component.toggleClass(_cssCls.collapsible, config.canCollapse); // update the component CSS class according to the scope $component.removeClass(scopeClass); scopeClass = _cssCls.scope[this.config.scope || _defaults.scope]; $component.addClass(scopeClass); return this; }, /** * Keep the active item visible, auto scroll if needed */ autoScroll: function autoScroll() { autoscroll(this.controls.$tree.find(_selectors.activeItem), this.controls.$tree); }, /** * 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 = this.getScopedMap(map, context); const testPart = mapHelper.getPart(map, context.testPartId); const progression = scopedMap.stats || { questions: 0, answered: 0, flagged: 0, viewed: 0, total: 0 }; const totalQuestions = this.getProgressionTotal(progression, 'questions'); this.map = map; this.progression = progression; // update the info panel this.writeCount(this.controls.$infoAnswered, progression.answered, totalQuestions); this.writeCount(this.controls.$infoUnanswered, totalQuestions - progression.answered, totalQuestions); this.writeCount(this.controls.$infoViewed, progression.viewed, this.getProgressionTotal(progression, 'total')); this.writeCount(this.controls.$infoFlagged, progression.flagged, totalQuestions); this.writeCount(this.controls.$infoAll, totalQuestions, null); // rebuild the tree if (!testPart.isLinear) { this.controls.$filterBar.show(); this.controls.$linearState.hide(); this.controls.$tree.html(navigatorTreeTpl(scopedMap)); this.autoScroll(); const activeItem = mapHelper.getActiveItem(scopedMap); this.setState('prevents-unseen', this.config.preventsUnseen); const isSkipaheadEnabled = activeItem && activeItem.categories && _.indexOf(activeItem.categories, 'x-tao-option-review-skipahead') >= 0; this.setState('skipahead-enabled', isSkipaheadEnabled); if (this.config.preventsUnseen && !isSkipaheadEnabled) { // disables all unseen items to prevent the test taker has access to. this.controls.$tree.find(_selectors.unseen).addClass(_cssCls.disabled); } } else { this.controls.$filterBar.hide(); this.controls.$linearState.show(); this.controls.$tree.empty(); } // apply again the current filter this.filter(this.controls.$filters.filter(_selectors.actives).data('mode')); /** * @event navigator#update the navigation data have changed */ this.trigger('update'); return this; }, /** * Gets the scoped map * @param {Object} map The current test map * @param {Object} context The current test context * @returns {object} The scoped map */ getScopedMap: function getScopedMap(map, context) { var scopedMap = mapHelper.getScopeMapFromContext(map, context, this.config.scope); var testPart = mapHelper.getPart(scopedMap, context.testPartId) || {}; var section = mapHelper.getSection(scopedMap, context.sectionId) || {}; var item = mapHelper.getItem(scopedMap, context.itemIdentifier) || {}; // set the active part/section/item testPart.active = true; section.active = true; item.active = true; //interactive item counter let counter = 0; // adjust each item with additional meta return mapHelper.each(scopedMap, function (itm) { var cls = []; var icon = ''; if (itm.active) { cls.push('active'); } if (itm.informational) { cls.push('info'); icon = icon || 'info'; } if (itm.flagged) { cls.push('flagged'); icon = icon || 'flagged'; } if (itm.answered) { cls.push('answered'); icon = icon || 'answered'; } if (itm.viewed) { cls.push('viewed'); icon = icon || 'viewed'; } else { cls.push('unseen'); icon = icon || 'unseen'; } if (!itm.informational) { counter += 1; itm.numberTest = counter; //item position in whole test from 1 } itm.cls = cls.join(' '); itm.icon = icon; }); }, /** * Updates a counter * @param {jQuery} $place * @param {Number} count * @param {Number|Null} total * @private */ writeCount: function writeCount($place, count, total) { var display = 0; if ($place.parent().hasClass('qti-navigator-tab')) { display = Math.max(count, 0); } else if (total > 0) { display = `${Math.min(count, total)}/${total}`; } $place.text(display); }, /** * Selects an item * @param {String|jQuery} position The item's position * @param {Boolean} [open] Forces the tree to be opened on the selected item * @returns {jQuery} Returns the selected item */ select: function select(position, open) { // find the item to select and extract its hierarchy var $tree = this.controls.$tree; var selected = position && position.jquery ? position : $tree.find(`[data-position=${position}]`); var hierarchy = selected.parentsUntil($tree); var previousPosition = 0; var $previous = $tree.find(_selectors.activeItem); if ($previous.length) { previousPosition = $previous.data('position'); } // collapse the full tree and open only the hierarchy of the selected item if (open) { this.openOnly(hierarchy); } // select the item $tree.find(_selectors.actives).removeClass(_cssCls.active); hierarchy.add(selected).addClass(_cssCls.active); position = selected.data('position'); /** * 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); return selected; }, /** * Opens the tree on the selected item only * @returns {jQuery} Returns the selected item */ openSelected: function openSelected() { // find the selected item and extract its hierarchy var $tree = this.controls.$tree; var selected = $tree.find(_selectors.items + _selectors.actives); var hierarchy = selected.parentsUntil($tree); // collapse the full tree and open only the hierarchy of the selected item this.openOnly(hierarchy); return selected; }, /** * Collapses the full tree and opens only the provided branch * @param {jQuery} opened The element to be opened * @param {jQuery} [root] The root element from which collapse the panels */ openOnly: function openOnly(opened, root) { (root || this.controls.$tree).find(_selectors.collapsible).addClass(_cssCls.collapsed); opened.removeClass(_cssCls.collapsed); }, /** * Toggles a panel * @param {jQuery} panel The panel to toggle * @param {String} [collapseSelector] Selector of panels to collapse * @returns {Boolean} Returns `true` if the panel just expanded now */ togglePanel: function togglePanel(panel, collapseSelector) { var collapsed = panel.hasClass(_cssCls.collapsed); if (collapseSelector) { this.controls.$tree.find(collapseSelector).addClass(_cssCls.collapsed); } if (collapsed) { panel.removeClass(_cssCls.collapsed); } else { panel.addClass(_cssCls.collapsed); } return collapsed; }, /** * Toggles the display state of the component * @param {Boolean} [show] External condition that's tells if the component must be shown or hidden * @returns {navigatorApi} */ toggle: function toggle(show) { if (typeof show === 'undefined') { show = this.is('hidden'); } if (show) { this.show(); } else { this.hide(); } return this; } }; /** * * @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 * @param {Boolean} [config.canCollapse] Allow the test taker to collapse the component * @param {Boolean} [config.canFlag] Allow the test taker to flag items * @param {Boolean} [config.hidden] Hide the component at init * @returns {*} */ function navigatorFactory(config) { var navigator; /** * Flags an item * @param {jQuery} $item */ function flagItem($item) { var position = $item.data('position'); var flagged = !$item.hasClass(_cssCls.flagged); // update the display navigator.setItemFlag(position, flagged); /** * An item is flagged * @event navigator#flag * @param {Number} position - The item position on which jump * @param {Boolean} flag - Tells whether the item is marked for review or not */ navigator.trigger('flag', position, flagged); } /** * Jumps to an item * @param {jQuery} $item * @private */ function jump($item) { var position = $item.data('position'); /** * A jump to a particular item is required * @event navigator#jump * @param {Number} position - The item position on which jump */ navigator.trigger('jump', position); } navigator = component(navigatorApi, _defaults).setTemplate(navigatorTpl) // uninstalls the component .on('destroy', function () { this.controls = null; }) // keep the activ item visible .on('show', function () { this.autoScroll(); }) // renders the component .on('render', function () { var self = this; // main component elements var $component = this.getElement(); var $filterBar = $component.find(_selectors.filterBar); var $filters = $filterBar.find('li'); var $tree = $component.find(_selectors.tree); // links the component to the underlying DOM elements this.controls = { // access to info panel displaying counters $infoAnswered: $component.find(_selectors.infoAnswered), $infoViewed: $component.find(_selectors.infoViewed), $infoAll: $component.find(_selectors.tabInfoAll), $infoUnanswered: this.config.showLegend ? $component.find(_selectors.infoUnanswered) : $component.find(_selectors.tabInfoUnanswered), $infoFlagged: this.config.showLegend ? $component.find(_selectors.infoFlagged) : $component.find(_selectors.tabInfoFlagged), // access to filter switches $filterBar: $filterBar, $filters: $filters, // access to the tree of parts/sections/items $tree: $tree, // access to the panel displayed when a linear part is reached $linearState: $component.find(_selectors.linearState) }; // apply options this.updateConfig(); // click on the collapse handle: collapse/expand the review panel $component.on(`click${_selectors.component}`, _selectors.collapseHandle, function () { if (!self.is('disabled')) { $component.toggleClass(_cssCls.collapsed); if ($component.hasClass(_cssCls.collapsed)) { self.openSelected(); } } }); // click on the info panel title: toggle the related panel $component.on(`click${_selectors.component}`, _selectors.infoPanelLabels, function () { if (!self.is('disabled')) { self.togglePanel($$1(this).closest(_selectors.infoPanel), _selectors.infoPanel); } }); // click on a part title: toggle the related panel $tree.on(`click${_selectors.component}`, _selectors.partLabels, function () { var $panel; if (!self.is('disabled')) { $panel = $$1(this).closest(_selectors.parts); if (self.togglePanel($panel, _selectors.parts)) { if ($panel.hasClass(_cssCls.active)) { self.openSelected(); } else { self.openOnly($panel.find(_selectors.sections).first(), $panel); } } } }); // click on a section title: toggle the related panel $tree.on(`click${_selectors.component}`, _selectors.sectionLabels, function () { if (!self.is('disabled')) { self.togglePanel($$1(this).closest(_selectors.sections), _selectors.sections); } }); // click on an item: jump to the position $tree.on(`click${_selectors.component}`, _selectors.itemLabels, function (event) { var $item, $target; if (!self.is('disabled')) { $item = $$1(this).closest(_selectors.items); if (!$item.hasClass(_cssCls.disabled)) { $target = $$1(event.target); if (self.config.canFlag && $target.is(_selectors.icons) && !$component.hasClass(_cssCls.collapsed)) { // click on the icon, just flag the item, unless the panel is collapsed if (!$item.hasClass(_cssCls.unseen) && !$item.hasClass(_cssCls.info)) { flagItem($item); } } else if (!$item.hasClass(_cssCls.active)) { // go to the selected item self.select($item); jump($item); } } } }); // click on the start button inside a linear part: jump to the position $tree.on(`click${_selectors.component}`, _selectors.linearStart, function () { var $btn; if (!self.is('disabled')) { $btn = $$1(this); // go to the first item of the linear part if (!$btn.hasClass(_cssCls.disabled)) { $btn.addClass(_cssCls.disabled); jump($btn); } } }); // click on a filter button $filterBar.on(`click${_selectors.component}`, _selectors.filter, function () { var $btn, mode; if (!self.is('disabled')) { $btn = $$1(this); mode = $btn.data('mode'); // select the button $filters.removeClass(_cssCls.active); $filters.attr('aria-selected', false); $component.removeClass(_cssCls.collapsed); $btn.addClass(_cssCls.active); $btn.attr('aria-selected', true); // filter the items self.filter(mode); //after filtering, ensure that the active item (if exists) is visible self.autoScroll(); } }); }); // set default filter navigator.currentFilter = 'all'; // the component will be ready return navigator.init(config); } return navigatorFactory; });