891 lines
31 KiB
JavaScript
891 lines
31 KiB
JavaScript
/**
|
|
* 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) 2015 (original work) Open Assessment Technologies SA ;
|
|
*/
|
|
/**
|
|
* @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
|
|
*/
|
|
define([
|
|
'jquery',
|
|
'lodash',
|
|
'i18n',
|
|
'tpl!taoQtiTest/testRunner/tpl/navigator',
|
|
'tpl!taoQtiTest/testRunner/tpl/navigatorTree',
|
|
'util/capitalize'
|
|
], function ($, _, __, navigatorTpl, navigatorTreeTpl, capitalize) {
|
|
'use strict';
|
|
|
|
/**
|
|
* 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',
|
|
icon : 'qti-navigator-icon',
|
|
scope : {
|
|
test : 'scope-test',
|
|
testPart : 'scope-test-part',
|
|
testSection : 'scope-test-section'
|
|
}
|
|
};
|
|
|
|
/**
|
|
* List of common CSS selectors
|
|
* @type {Object}
|
|
* @private
|
|
*/
|
|
var _selectors = {
|
|
component : '.qti-navigator',
|
|
filterBar : '.qti-navigator-filters',
|
|
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',
|
|
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',
|
|
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)',
|
|
hidden : '.hidden'
|
|
};
|
|
|
|
/**
|
|
* 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,
|
|
flagged : _selectors.notFlagged,
|
|
answered : _selectors.notAnswered,
|
|
filtered : _selectors.hidden
|
|
};
|
|
|
|
/**
|
|
* Maps of config options translated from the context object to the local options
|
|
* @type {Object}
|
|
* @private
|
|
*/
|
|
var _optionsMap = {
|
|
'reviewScope' : 'reviewScope',
|
|
'reviewPreventsUnseen' : 'preventsUnseen',
|
|
'canCollapse' : 'canCollapse'
|
|
};
|
|
|
|
/**
|
|
* Maps the handled review scopes
|
|
* @type {Object}
|
|
* @private
|
|
*/
|
|
var _reviewScopes = {
|
|
test : 'test',
|
|
testPart : 'testPart',
|
|
testSection : 'testSection'
|
|
};
|
|
|
|
/**
|
|
* Provides a test review manager
|
|
* @type {Object}
|
|
*/
|
|
var testReview = {
|
|
/**
|
|
* Initializes the component
|
|
* @param {String|jQuery|HTMLElement} element The element on which install the component
|
|
* @param {Object} [options] A list of extra options
|
|
* @param {String} [options.region] The region on which put the component: left or right
|
|
* @param {String} [options.reviewScope] Limit the review screen to a particular scope:
|
|
* the whole test, the current test part or the current test section)
|
|
* @param {Boolean} [options.preventsUnseen] Prevents the test taker to access unseen items
|
|
* @returns {testReview}
|
|
*/
|
|
init: function init(element, options) {
|
|
var initOptions = _.isObject(options) && options || {};
|
|
var putOnRight = 'right' === initOptions.region;
|
|
var insertMethod = putOnRight ? 'append' : 'prepend';
|
|
|
|
this.options = initOptions;
|
|
this.disabled = false;
|
|
this.hidden = !!initOptions.hidden;
|
|
this.currentFilter = 'all';
|
|
|
|
// clean the DOM if the init method is called after initialisation
|
|
if (this.$component) {
|
|
this.$component.remove();
|
|
}
|
|
|
|
// build the component structure and inject it into the DOM
|
|
this.$container = $(element);
|
|
insertMethod = this.$container[insertMethod];
|
|
if (insertMethod) {
|
|
insertMethod.call(this.$container, navigatorTpl({
|
|
region: putOnRight ? 'right' : 'left',
|
|
hidden: this.hidden
|
|
}));
|
|
} else {
|
|
throw new Error("Unable to inject the component structure into the DOM");
|
|
}
|
|
|
|
// install the component behaviour
|
|
this._loadDOM();
|
|
this._initEvents();
|
|
this._updateDisplayOptions();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Links the component to the underlying DOM elements
|
|
* @private
|
|
*/
|
|
_loadDOM: function() {
|
|
this.$component = this.$container.find(_selectors.component);
|
|
|
|
// access to info panel displaying counters
|
|
this.$infoAnswered = this.$component.find(_selectors.infoAnswered);
|
|
this.$infoViewed = this.$component.find(_selectors.infoViewed);
|
|
this.$infoUnanswered = this.$component.find(_selectors.infoUnanswered);
|
|
this.$infoFlagged = this.$component.find(_selectors.infoFlagged);
|
|
|
|
// access to filter switches
|
|
this.$filterBar = this.$component.find(_selectors.filterBar);
|
|
this.$filters = this.$filterBar.find('li');
|
|
|
|
// access to the tree of parts/sections/items
|
|
this.$tree = this.$component.find(_selectors.tree);
|
|
|
|
// access to the panel displayed when a linear part is reached
|
|
this.$linearState = this.$component.find(_selectors.linearState);
|
|
},
|
|
|
|
/**
|
|
* Installs the event handlers on the underlying DOM elements
|
|
* @private
|
|
*/
|
|
_initEvents: function() {
|
|
var self = this;
|
|
|
|
// click on the collapse handle: collapse/expand the review panel
|
|
this.$component.on('click' + _selectors.component, _selectors.collapseHandle, function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
self.$component.toggleClass(_cssCls.collapsed);
|
|
if (self.$component.hasClass(_cssCls.collapsed)) {
|
|
self._openSelected();
|
|
}
|
|
});
|
|
|
|
// click on the info panel title: toggle the related panel
|
|
this.$component.on('click' + _selectors.component, _selectors.infoPanelLabels, function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $panel = $(this).closest(_selectors.infoPanel);
|
|
self._togglePanel($panel, _selectors.infoPanel);
|
|
});
|
|
|
|
// click on a part title: toggle the related panel
|
|
this.$tree.on('click' + _selectors.component, _selectors.partLabels, function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $panel = $(this).closest(_selectors.parts);
|
|
var open = self._togglePanel($panel, _selectors.parts);
|
|
|
|
if (open) {
|
|
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
|
|
this.$tree.on('click' + _selectors.component, _selectors.sectionLabels, function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $panel = $(this).closest(_selectors.sections);
|
|
|
|
self._togglePanel($panel, _selectors.sections);
|
|
});
|
|
|
|
// click on an item: jump to the position
|
|
this.$tree.on('click' + _selectors.component, _selectors.itemLabels, function(event) {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $item = $(this).closest(_selectors.items);
|
|
var $target;
|
|
|
|
if (!$item.hasClass(_cssCls.disabled)) {
|
|
$target = $(event.target);
|
|
if ($target.is(_selectors.icons) && !self.$component.hasClass(_cssCls.collapsed)) {
|
|
if (!$item.hasClass(_cssCls.unseen)) {
|
|
self._mark($item);
|
|
}
|
|
} else {
|
|
self._select($item);
|
|
self._jump($item);
|
|
}
|
|
}
|
|
});
|
|
|
|
// click on the start button inside a linear part: jump to the position
|
|
this.$tree.on('click' + _selectors.component, _selectors.linearStart, function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $btn = $(this);
|
|
|
|
if (!$btn.hasClass(_cssCls.disabled)) {
|
|
$btn.addClass(_cssCls.disabled);
|
|
self._jump($btn);
|
|
}
|
|
});
|
|
|
|
// click on a filter button
|
|
this.$filterBar.on('click' + _selectors.component, 'li', function() {
|
|
if (self.disabled) {
|
|
return;
|
|
}
|
|
|
|
var $btn = $(this);
|
|
var mode = $btn.data('mode');
|
|
|
|
self.$filters.removeClass(_cssCls.active);
|
|
self.$component.removeClass(_cssCls.collapsed);
|
|
$btn.addClass(_cssCls.active);
|
|
|
|
self._filter(mode);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Filters the items by a criteria
|
|
* @param {String} criteria
|
|
* @private
|
|
*/
|
|
_filter: function(criteria) {
|
|
var $items = this.$tree.find(_selectors.items).removeClass(_cssCls.hidden);
|
|
var filter = _filterMap[criteria];
|
|
if (filter) {
|
|
$items.filter(filter).addClass(_cssCls.hidden);
|
|
}
|
|
this._updateSectionCounters(!!filter);
|
|
this.currentFilter = criteria;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @private
|
|
*/
|
|
_select: function(position, open) {
|
|
// find the item to select and extract its hierarchy
|
|
var selected = position && position.jquery ? position : this.$tree.find('[data-position=' + position + ']');
|
|
var hierarchy = selected.parentsUntil(this.$tree);
|
|
|
|
// collapse the full tree and open only the hierarchy of the selected item
|
|
if (open) {
|
|
this._openOnly(hierarchy);
|
|
}
|
|
|
|
// select the item
|
|
this.$tree.find(_selectors.actives).removeClass(_cssCls.active);
|
|
hierarchy.add(selected).addClass(_cssCls.active);
|
|
return selected;
|
|
},
|
|
|
|
/**
|
|
* Opens the tree on the selected item only
|
|
* @returns {jQuery} Returns the selected item
|
|
* @private
|
|
*/
|
|
_openSelected: function() {
|
|
// find the selected item and extract its hierarchy
|
|
var selected = this.$tree.find(_selectors.items + _selectors.actives);
|
|
var hierarchy = selected.parentsUntil(this.$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
|
|
* @private
|
|
*/
|
|
_openOnly: function(opened, root) {
|
|
(root || this.$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(panel, collapseSelector) {
|
|
var collapsed = panel.hasClass(_cssCls.collapsed);
|
|
|
|
if (collapseSelector) {
|
|
this.$tree.find(collapseSelector).addClass(_cssCls.collapsed);
|
|
}
|
|
|
|
if (collapsed) {
|
|
panel.removeClass(_cssCls.collapsed);
|
|
} else {
|
|
panel.addClass(_cssCls.collapsed);
|
|
}
|
|
return collapsed;
|
|
},
|
|
|
|
/**
|
|
* Sets the icon of a particular item
|
|
* @param {jQuery} $item
|
|
* @param {String} icon
|
|
* @private
|
|
*/
|
|
_setItemIcon: function($item, icon) {
|
|
$item.find(_selectors.icons).attr('class', _cssCls.icon + ' icon-' + icon);
|
|
},
|
|
|
|
/**
|
|
* Sets the icon of a particular item according to its state
|
|
* @param {jQuery} $item
|
|
* @private
|
|
*/
|
|
_adjustItemIcon: function($item) {
|
|
var icon = null;
|
|
var defaultIcon = _cssCls.unseen;
|
|
var iconCls = [
|
|
_cssCls.flagged,
|
|
_cssCls.answered,
|
|
_cssCls.viewed
|
|
];
|
|
|
|
_.forEach(iconCls, function(cls) {
|
|
if ($item.hasClass(cls)) {
|
|
icon = cls;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this._setItemIcon($item, icon || defaultIcon);
|
|
},
|
|
|
|
/**
|
|
* Toggle the marked state of an item
|
|
* @param {jQuery} $item
|
|
* @param {Boolean} [flag]
|
|
* @private
|
|
*/
|
|
_toggleFlag: function($item, flag) {
|
|
$item.toggleClass(_cssCls.flagged, flag);
|
|
this._adjustItemIcon($item);
|
|
},
|
|
|
|
/**
|
|
* Marks an item for later review
|
|
* @param {jQuery} $item
|
|
* @private
|
|
*/
|
|
_mark: function($item) {
|
|
var itemId = $item.data('id');
|
|
var itemPosition = $item.data('position');
|
|
var flag = !$item.hasClass(_cssCls.flagged);
|
|
|
|
this._toggleFlag($item);
|
|
|
|
/**
|
|
* A storage of the flag is required
|
|
* @event testReview#mark
|
|
* @param {Boolean} flag - Tells whether the item is marked for review or not
|
|
* @param {Number} position - The item position on which jump
|
|
* @param {String} itemId - The identifier of the target item
|
|
* @param {testReview} testReview - The client testReview component
|
|
*/
|
|
this.trigger('mark', [flag, itemPosition, itemId]);
|
|
},
|
|
|
|
/**
|
|
* Jumps to an item
|
|
* @param {jQuery} $item
|
|
* @private
|
|
*/
|
|
_jump: function($item) {
|
|
var itemId = $item.data('id');
|
|
var itemPosition = $item.data('position');
|
|
|
|
/**
|
|
* A jump to a particular item is required
|
|
* @event testReview#jump
|
|
* @param {Number} position - The item position on which jump
|
|
* @param {String} itemId - The identifier of the target item
|
|
* @param {testReview} testReview - The client testReview component
|
|
*/
|
|
this.trigger('jump', [itemPosition, itemId]);
|
|
},
|
|
|
|
/**
|
|
* Updates the sections related items counters
|
|
* @param {Boolean} filtered
|
|
*/
|
|
_updateSectionCounters: function(filtered) {
|
|
var self = this;
|
|
var filter = _filterMap[filtered ? 'filtered' : 'answered'];
|
|
this.$tree.find(_selectors.sections).each(function() {
|
|
var $section = $(this);
|
|
var $items = $section.find(_selectors.items);
|
|
var $filtered = $items.filter(filter);
|
|
var total = $items.length;
|
|
var nb = total - $filtered.length;
|
|
self._writeCount($section.find(_selectors.counters), nb, total);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Updates the display according to options
|
|
* @private
|
|
*/
|
|
_updateDisplayOptions: function() {
|
|
var reviewScope = _reviewScopes[this.options.reviewScope] || 'test';
|
|
var scopeClass = _cssCls.scope[reviewScope];
|
|
var $root = this.$component;
|
|
_.forEach(_cssCls.scope, function(cls) {
|
|
$root.removeClass(cls);
|
|
});
|
|
if (scopeClass) {
|
|
$root.addClass(scopeClass);
|
|
}
|
|
$root.toggleClass(_cssCls.collapsible, this.options.canCollapse);
|
|
},
|
|
|
|
/**
|
|
* Updates the local options from the provided context
|
|
* @param {Object} testContext The progression context
|
|
* @private
|
|
*/
|
|
_updateOptions: function(testContext) {
|
|
var options = this.options;
|
|
_.forEach(_optionsMap, function(optionKey, contextKey) {
|
|
if (undefined !== testContext[contextKey]) {
|
|
options[optionKey] = testContext[contextKey];
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Updates the info panel
|
|
*/
|
|
_updateInfos: function() {
|
|
var progression = this.progression,
|
|
unanswered = Number(progression.total) - Number(progression.answered);
|
|
|
|
// update the info panel
|
|
this._writeCount(this.$infoAnswered, progression.answered, progression.total);
|
|
this._writeCount(this.$infoUnanswered, unanswered, progression.total);
|
|
this._writeCount(this.$infoViewed, progression.viewed, progression.total);
|
|
this._writeCount(this.$infoFlagged, progression.flagged, progression.total);
|
|
},
|
|
|
|
/**
|
|
* Updates a counter
|
|
* @param {jQuery} $place
|
|
* @param {Number} count
|
|
* @param {Number} total
|
|
* @private
|
|
*/
|
|
_writeCount: function($place, count, total) {
|
|
$place.text(count + '/' + total);
|
|
},
|
|
|
|
/**
|
|
* Gets the progression stats for the whole test
|
|
* @param {Object} testContext The progression context
|
|
* @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}
|
|
* @private
|
|
*/
|
|
_getProgressionOfTest: function(testContext) {
|
|
return {
|
|
total : testContext.numberItems || 0,
|
|
answered : testContext.numberCompleted || 0,
|
|
viewed : testContext.numberPresented || 0,
|
|
flagged : testContext.numberFlagged || 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Gets the progression stats for the current test part
|
|
* @param {Object} testContext The progression context
|
|
* @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}
|
|
* @private
|
|
*/
|
|
_getProgressionOfTestPart: function(testContext) {
|
|
return {
|
|
total : testContext.numberItemsPart || 0,
|
|
answered : testContext.numberCompletedPart || 0,
|
|
viewed : testContext.numberPresentedPart || 0,
|
|
flagged : testContext.numberFlaggedPart || 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Gets the progression stats for the current test section
|
|
* @param {Object} testContext The progression context
|
|
* @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}
|
|
* @private
|
|
*/
|
|
_getProgressionOfTestSection: function(testContext) {
|
|
return {
|
|
total : testContext.numberItemsSection || 0,
|
|
answered : testContext.numberCompletedSection || 0,
|
|
viewed : testContext.numberPresentedSection || 0,
|
|
flagged : testContext.numberFlaggedSection || 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Updates the navigation tre
|
|
* @param {Object} testContext The progression context
|
|
*/
|
|
_updateTree: function(testContext) {
|
|
var navigatorMap = testContext.navigatorMap;
|
|
var reviewScope = this.options.reviewScope;
|
|
var reviewScopePart = reviewScope === 'testPart';
|
|
var reviewScopeSection = reviewScope === 'testSection';
|
|
var _partsFilter = function(part) {
|
|
if (reviewScopeSection && part.sections) {
|
|
part.sections = _.filter(part.sections, _partsFilter);
|
|
}
|
|
return part.active;
|
|
};
|
|
|
|
// rebuild the tree
|
|
if (navigatorMap) {
|
|
if (reviewScopePart || reviewScopeSection) {
|
|
// display only the current section
|
|
navigatorMap = _.filter(navigatorMap, _partsFilter);
|
|
}
|
|
|
|
this.$filterBar.show();
|
|
this.$linearState.hide();
|
|
this.$tree.html(navigatorTreeTpl({
|
|
parts: navigatorMap
|
|
}));
|
|
|
|
if (this.options.preventsUnseen) {
|
|
// disables all unseen items to prevent the test taker has access to.
|
|
this.$tree.find(_selectors.unseen).addClass(_cssCls.disabled);
|
|
}
|
|
} else {
|
|
this.$filterBar.hide();
|
|
this.$linearState.show();
|
|
this.$tree.empty();
|
|
}
|
|
|
|
// apply again the current filter
|
|
this._filter(this.$filters.filter(_selectors.actives).data('mode'));
|
|
},
|
|
|
|
/**
|
|
* 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.$tree.find('[data-position=' + position + ']');
|
|
var progression = this.progression;
|
|
|
|
// update the item flag
|
|
this._toggleFlag($item, flag);
|
|
|
|
// update the info panel
|
|
progression.flagged = this.$tree.find(_selectors.flagged).length;
|
|
this._writeCount(this.$infoFlagged, progression.flagged, progression.total);
|
|
this._filter(this.currentFilter);
|
|
},
|
|
|
|
/**
|
|
* Update the number of flagged items in the test context
|
|
* @param {Object} testContext The test context
|
|
* @param {Number} position The position of the flagged item
|
|
* @param {Boolean} flag The flag state
|
|
*/
|
|
updateNumberFlagged: function(testContext, position, flag) {
|
|
var fields = ['numberFlagged'];
|
|
var currentPosition = testContext.itemPosition;
|
|
var currentFound = false, currentSection = null, currentPart = null;
|
|
var itemFound = false, itemSection = null, itemPart = null;
|
|
|
|
if (testContext.navigatorMap) {
|
|
// find the current item and the marked item inside the navigator map
|
|
// check if the marked item is in the current section
|
|
_.forEach(testContext.navigatorMap, function(part) {
|
|
_.forEach(part && part.sections, function(section) {
|
|
_.forEach(section && section.items, function(item) {
|
|
if (item) {
|
|
if (item.position === position) {
|
|
itemPart = part;
|
|
itemSection = section;
|
|
itemFound = true;
|
|
}
|
|
if (item.position === currentPosition) {
|
|
currentPart = part;
|
|
currentSection = section;
|
|
currentFound = true;
|
|
|
|
}
|
|
if (itemFound && currentFound) {
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (itemFound && currentFound) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (itemFound && currentFound) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// select the context to update
|
|
if (itemFound && currentPart === itemPart) {
|
|
fields.push('numberFlaggedPart');
|
|
}
|
|
if (itemFound && currentSection === itemSection) {
|
|
fields.push('numberFlaggedSection');
|
|
}
|
|
} else {
|
|
// no navigator map, the current the marked item is in the current section
|
|
fields.push('numberFlaggedPart');
|
|
fields.push('numberFlaggedSection');
|
|
}
|
|
|
|
_.forEach(fields, function(field) {
|
|
if (field in testContext) {
|
|
testContext[field] += flag ? 1 : -1;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Get progression
|
|
* @param {Object} testContext The progression context
|
|
* @returns {object} progression
|
|
*/
|
|
getProgression: function getProgression(testContext) {
|
|
var reviewScope = _reviewScopes[this.options.reviewScope] || 'test',
|
|
progressInfoMethod = '_getProgressionOf' + capitalize(reviewScope),
|
|
getProgression = this[progressInfoMethod] || this._getProgressionOfTest,
|
|
progression = getProgression && getProgression(testContext) || {};
|
|
|
|
return progression;
|
|
},
|
|
|
|
/**
|
|
* Updates the review screen
|
|
* @param {Object} testContext The progression context
|
|
* @returns {testReview}
|
|
*/
|
|
update: function update(testContext) {
|
|
this.progression = this.getProgression(testContext);
|
|
this._updateOptions(testContext);
|
|
this._updateInfos(testContext);
|
|
this._updateTree(testContext);
|
|
this._updateDisplayOptions(testContext);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Disables the component
|
|
* @returns {testReview}
|
|
*/
|
|
disable: function disable() {
|
|
this.disabled = true;
|
|
this.$component.addClass(_cssCls.disabled);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Enables the component
|
|
* @returns {testReview}
|
|
*/
|
|
enable: function enable() {
|
|
this.disabled = false;
|
|
this.$component.removeClass(_cssCls.disabled);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Hides the component
|
|
* @returns {testReview}
|
|
*/
|
|
hide: function hide() {
|
|
this.disabled = true;
|
|
this.hidden = true;
|
|
this.$component.addClass(_cssCls.hidden);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Shows the component
|
|
* @returns {testReview}
|
|
*/
|
|
show: function show() {
|
|
this.disabled = false;
|
|
this.hidden = false;
|
|
this.$component.removeClass(_cssCls.hidden);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Toggles the display state of the component
|
|
* @param {Boolean} [show] External condition that's tells if the component must be shown or hidden
|
|
* @returns {testReview}
|
|
*/
|
|
toggle: function toggle(show) {
|
|
if (undefined === show) {
|
|
show = this.hidden;
|
|
}
|
|
|
|
if (show) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Install an event handler on the underlying DOM element
|
|
* @param {String} eventName
|
|
* @returns {testReview}
|
|
*/
|
|
on: function on(eventName) {
|
|
var dom = this.$component;
|
|
if (dom) {
|
|
dom.on.apply(dom, arguments);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Uninstall an event handler from the underlying DOM element
|
|
* @param {String} eventName
|
|
* @returns {testReview}
|
|
*/
|
|
off: function off(eventName) {
|
|
var dom = this.$component;
|
|
if (dom) {
|
|
dom.off.apply(dom, arguments);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Triggers an event on the underlying DOM element
|
|
* @param {String} eventName
|
|
* @param {Array|Object} extraParameters
|
|
* @returns {testReview}
|
|
*/
|
|
trigger : function trigger(eventName, extraParameters) {
|
|
var dom = this.$component;
|
|
|
|
if (undefined === extraParameters) {
|
|
extraParameters = [];
|
|
}
|
|
if (!_.isArray(extraParameters)) {
|
|
extraParameters = [extraParameters];
|
|
}
|
|
|
|
extraParameters.push(this);
|
|
|
|
if (dom) {
|
|
dom.trigger(eventName, extraParameters);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds an instance of testReview
|
|
* @param {String|jQuery|HTMLElement} element The element on which install the component
|
|
* @param {Object} [options] A list of extra options
|
|
* @param {String} [options.region] The region on which put the component: left or right
|
|
* @param {String} [options.reviewScope] Limit the review screen to a particular scope:
|
|
* the whole test, the current test part or the current test section)
|
|
* @param {Boolean} [options.preventsUnseen] Prevents the test taker to access unseen items
|
|
* @returns {testReview}
|
|
*/
|
|
var testReviewFactory = function(element, options) {
|
|
var component = _.clone(testReview, true);
|
|
return component.init(element, options);
|
|
};
|
|
|
|
return testReviewFactory;
|
|
});
|