/g, '')
.replace(/<\/p>/g, '')
.trim();
if (!tipContent.length) {
return; // Empty tip
}
else {
$(this).addClass('h5p-has-tip');
}
// Add tip
var $wrap = $('
', {
'role': 'button',
'tabindex': 0,
'title': params.UI.tipsLabel,
'aria-label': params.UI.tipsLabel,
'aria-expanded': false,
'class': 'multichoice-tip',
appendTo: $wrap
});
var tipIconHtml = '
' +
'' +
'' +
'' +
'';
$multichoiceTip.append(tipIconHtml);
$multichoiceTip.click(function () {
var $tipContainer = $multichoiceTip.parents('.h5p-answer');
var openFeedback = !$tipContainer.children('.h5p-feedback-dialog').is($feedbackDialog);
removeFeedbackDialog();
// Do not open feedback if it was open
if (openFeedback) {
$multichoiceTip.attr('aria-expanded', true);
// Add tip dialog
addFeedback($tipContainer, tip);
$feedbackDialog.addClass('h5p-has-tip');
// Tip for readspeaker
self.read(tip);
}
else {
$multichoiceTip.attr('aria-expanded', false);
}
self.trigger('resize');
// Remove tip dialog on dom click
setTimeout(function () {
$myDom.click(removeFeedbackDialog);
}, 100);
// Do not propagate
return false;
}).keydown(function (e) {
if (e.which === 32) {
$(this).click();
return false;
}
});
$('.h5p-alternative-container', this).append($wrap);
});
// Set event listeners.
var toggleCheck = function ($ans) {
if ($ans.attr('aria-disabled') === 'true') {
return;
}
self.answered = true;
var num = parseInt($ans.data('id'));
if (params.behaviour.singleAnswer) {
// Store answer
params.userAnswers = [num];
// Calculate score
score = (params.answers[num].correct ? 1 : 0);
// De-select previous answer
$answers.not($ans).removeClass('h5p-selected').attr('tabindex', '-1').attr('aria-checked', 'false');
// Select new answer
$ans.addClass('h5p-selected').attr('tabindex', '0').attr('aria-checked', 'true');
}
else {
if ($ans.attr('aria-checked') === 'true') {
const pos = params.userAnswers.indexOf(num);
if (pos !== -1) {
params.userAnswers.splice(pos, 1);
}
// Do not allow un-checking when retry disabled and auto check
if (params.behaviour.autoCheck && !params.behaviour.enableRetry) {
return;
}
// Remove check
$ans.removeClass('h5p-selected').attr('aria-checked', 'false');
}
else {
params.userAnswers.push(num);
$ans.addClass('h5p-selected').attr('aria-checked', 'true');
}
// Calculate score
calcScore();
}
self.triggerXAPI('interacted');
hideSolution($ans);
if (params.userAnswers.length) {
self.showButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
if (params.behaviour.autoCheck) {
if (params.behaviour.singleAnswer) {
// Only a single answer allowed
checkAnswer();
}
else {
// Show feedback for selected alternatives
self.showCheckSolution(true);
// Always finish task if it was completed successfully
if (score === self.getMaxScore()) {
checkAnswer();
}
}
}
}
};
$answers.click(function () {
toggleCheck($(this));
}).keydown(function (e) {
if (e.keyCode === 32) { // Space bar
// Select current item
toggleCheck($(this));
return false;
}
if (params.behaviour.singleAnswer) {
switch (e.keyCode) {
case 38: // Up
case 37: { // Left
// Try to select previous item
var $prev = $(this).prev();
if ($prev.length) {
toggleCheck($prev.focus());
}
return false;
}
case 40: // Down
case 39: { // Right
// Try to select next item
var $next = $(this).next();
if ($next.length) {
toggleCheck($next.focus());
}
return false;
}
}
}
});
if (params.behaviour.singleAnswer) {
// Special focus handler for radio buttons
$answers.focus(function () {
if ($(this).attr('aria-disabled') !== 'true') {
$answers.not(this).attr('tabindex', '-1');
}
}).blur(function () {
if (!$answers.filter('.h5p-selected').length) {
$answers.first().add($answers.last()).attr('tabindex', '0');
}
});
}
// Adds check and retry button
addButtons();
if (!params.behaviour.singleAnswer) {
calcScore();
}
else {
if (params.userAnswers.length && params.answers[params.userAnswers[0]].correct) {
score = 1;
}
else {
score = 0;
}
}
// Has answered through auto-check in a previous session
if (hasCheckedAnswer && params.behaviour.autoCheck) {
// Check answers if answer has been given or max score reached
if (params.behaviour.singleAnswer || score === self.getMaxScore()) {
checkAnswer();
}
else {
// Show feedback for checked checkboxes
self.showCheckSolution(true);
}
}
};
this.showAllSolutions = function () {
if (solutionsVisible) {
return;
}
solutionsVisible = true;
$myDom.find('.h5p-answer').each(function (i, e) {
var $e = $(e);
var a = params.answers[i];
if (a.correct) {
$e.addClass('h5p-should').append($('
', {
'class': 'h5p-solution-icon',
html: params.UI.shouldCheck + '.'
}));
}
else {
$e.addClass('h5p-should-not').append($('
', {
'class': 'h5p-solution-icon',
html: params.UI.shouldNotCheck + '.'
}));
}
}).find('.h5p-question-plus-one, .h5p-question-minus-one').remove();
// Make sure input is disabled in solution mode
disableInput();
// Move focus back to the first correct alternative so that the user becomes
// aware that the solution is being shown.
$myDom.find('.h5p-answer.h5p-should').first().focus();
//Hide buttons and retry depending on settings.
self.hideButton('check-answer');
self.hideButton('show-solution');
if (params.behaviour.enableRetry) {
self.showButton('try-again');
}
self.trigger('resize');
};
/**
* Used in contracts.
* Shows the solution for the task and hides all buttons.
*/
this.showSolutions = function () {
removeFeedbackDialog();
self.showCheckSolution();
self.showAllSolutions();
disableInput();
self.hideButton('try-again');
};
/**
* Hide solution for the given answer(s)
*
* @private
* @param {H5P.jQuery} $answer
*/
var hideSolution = function ($answer) {
$answer
.removeClass('h5p-correct')
.removeClass('h5p-wrong')
.removeClass('h5p-should')
.removeClass('h5p-should-not')
.removeClass('h5p-has-feedback')
.find('.h5p-question-plus-one, .h5p-question-minus-one, .h5p-answer-icon, .h5p-solution-icon, .h5p-feedback-dialog').remove();
};
/**
*
*/
this.hideSolutions = function () {
solutionsVisible = false;
hideSolution($('.h5p-answer', $myDom));
this.removeFeedback(); // Reset feedback
self.trigger('resize');
};
/**
* Resets the whole task.
* Used in contracts with integrated content.
* @private
*/
this.resetTask = function () {
self.answered = false;
self.hideSolutions();
params.userAnswers = [];
removeSelections();
self.showButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
enableInput();
$myDom.find('.h5p-feedback-available').remove();
};
var calculateMaxScore = function () {
if (blankIsCorrect) {
return params.weight;
}
var maxScore = 0;
for (var i = 0; i < params.answers.length; i++) {
var choice = params.answers[i];
if (choice.correct) {
maxScore += (choice.weight !== undefined ? choice.weight : 1);
}
}
return maxScore;
};
this.getMaxScore = function () {
return (!params.behaviour.singleAnswer && !params.behaviour.singlePoint ? calculateMaxScore() : params.weight);
};
/**
* Check answer
*/
var checkAnswer = function () {
// Unbind removal of feedback dialogs on click
$myDom.unbind('click', removeFeedbackDialog);
// Remove all tip dialogs
removeFeedbackDialog();
if (params.behaviour.enableSolutionsButton) {
self.showButton('show-solution');
}
if (params.behaviour.enableRetry) {
self.showButton('try-again');
}
self.hideButton('check-answer');
self.showCheckSolution();
disableInput();
var xAPIEvent = self.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
addResponseToXAPI(xAPIEvent);
self.trigger(xAPIEvent);
};
/**
* Determine if any of the radios or checkboxes have been checked.
*
* @return {boolean}
*/
var isAnswerSelected = function () {
return !!$('.h5p-answer[aria-checked="true"]', $myDom).length;
};
/**
* Adds the ui buttons.
* @private
*/
var addButtons = function () {
var $content = $('[data-content-id="' + self.contentId + '"].h5p-content');
var $containerParents = $content.parents('.h5p-container');
// select find container to attach dialogs to
var $container;
if($containerParents.length !== 0) {
// use parent highest up if any
$container = $containerParents.last();
}
else if($content.length !== 0){
$container = $content;
}
else {
$container = $(document.body);
}
// Show solution button
self.addButton('show-solution', params.UI.showSolutionButton, function () {
if (params.behaviour.showSolutionsRequiresInput && !isAnswerSelected()) {
// Require answer before solution can be viewed
self.updateFeedbackContent(params.UI.noInput);
self.read(params.UI.noInput);
}
else {
calcScore();
self.showAllSolutions();
}
}, false, {
'aria-label': params.UI.a11yShowSolution,
});
// Check solution button
if (params.behaviour.enableCheckButton && (!params.behaviour.autoCheck || !params.behaviour.singleAnswer)) {
self.addButton('check-answer', params.UI.checkAnswerButton,
function () {
self.answered = true;
checkAnswer();
},
true,
{
'aria-label': params.UI.a11yCheck,
},
{
confirmationDialog: {
enable: params.behaviour.confirmCheckDialog,
l10n: params.confirmCheck,
instance: self,
$parentElement: $container
},
contentData: self.contentData,
textIfSubmitting: params.UI.submitAnswerButton,
}
);
}
// Try Again button
self.addButton('try-again', params.UI.tryAgainButton, function () {
self.resetTask();
if (params.behaviour.randomAnswers) {
// reshuffle answers
var oldIdMap = idMap;
idMap = getShuffleMap();
var answersDisplayed = $myDom.find('.h5p-answer');
// remember tips
var tip = [];
for (i = 0; i < answersDisplayed.length; i++) {
tip[i] = $(answersDisplayed[i]).find('.h5p-multichoice-tipwrap');
}
// Those two loops cannot be merged or you'll screw up your tips
for (i = 0; i < answersDisplayed.length; i++) {
// move tips and answers on display
$(answersDisplayed[i]).find('.h5p-alternative-inner').html(params.answers[i].text);
$(tip[i]).detach().appendTo($(answersDisplayed[idMap.indexOf(oldIdMap[i])]).find('.h5p-alternative-container'));
}
}
}, false, {
'aria-label': params.UI.a11yRetry,
}, {
confirmationDialog: {
enable: params.behaviour.confirmRetryDialog,
l10n: params.confirmRetry,
instance: self,
$parentElement: $container
}
});
};
/**
* @private
*/
var insertFeedback = function ($e, feedback) {
// Add visuals
addFeedback($e, feedback);
// Add button for readspeakers
var $wrap = $('
', {
'class': 'h5p-hidden-read h5p-feedback-available',
'aria-label': params.UI.feedbackAvailable + '.'
});
$('
', {
'role': 'button',
'tabindex': 0,
'aria-label': params.UI.readFeedback + '.',
appendTo: $wrap,
on: {
keydown: function (e) {
if (e.which === 32) { // Space
self.read(feedback);
return false;
}
}
}
});
$wrap.appendTo($e);
};
/**
* Determine which feedback text to display
*
* @param {number} score
* @param {number} max
* @return {string}
*/
var getFeedbackText = function (score, max) {
var ratio = (score / max);
var feedback = H5P.Question.determineOverallFeedback(params.overallFeedback, ratio);
return feedback.replace('@score', score).replace('@total', max);
};
/**
* Shows feedback on the selected fields.
* @public
* @param {boolean} [skipFeedback] Skip showing feedback if true
*/
this.showCheckSolution = function (skipFeedback) {
var scorePoints;
if (!(params.behaviour.singleAnswer || params.behaviour.singlePoint || !params.behaviour.showScorePoints)) {
scorePoints = new H5P.Question.ScorePoints();
}
$myDom.find('.h5p-answer').each(function (i, e) {
var $e = $(e);
var a = params.answers[i];
var chosen = ($e.attr('aria-checked') === 'true');
if (chosen) {
if (a.correct) {
// May already have been applied by instant feedback
if (!$e.hasClass('h5p-correct')) {
$e.addClass('h5p-correct').append($('
', {
'class': 'h5p-answer-icon',
html: params.UI.correctAnswer + '.'
}));
}
}
else {
if (!$e.hasClass('h5p-wrong')) {
$e.addClass('h5p-wrong').append($('
', {
'class': 'h5p-answer-icon',
html: params.UI.wrongAnswer + '.'
}));
}
}
if (scorePoints) {
var alternativeContainer = $e[0].querySelector('.h5p-alternative-container');
if (!params.behaviour.autoCheck || alternativeContainer.querySelector('.h5p-question-plus-one, .h5p-question-minus-one') === null) {
alternativeContainer.appendChild(scorePoints.getElement(a.correct));
}
}
}
if (!skipFeedback) {
if (chosen && a.tipsAndFeedback.chosenFeedback !== undefined && a.tipsAndFeedback.chosenFeedback !== '') {
insertFeedback($e, a.tipsAndFeedback.chosenFeedback);
}
else if (!chosen && a.tipsAndFeedback.notChosenFeedback !== undefined && a.tipsAndFeedback.notChosenFeedback !== '') {
insertFeedback($e, a.tipsAndFeedback.notChosenFeedback);
}
}
});
// Determine feedback
var max = self.getMaxScore();
// Disable task if maxscore is achieved
var fullScore = (score === max);
if (fullScore) {
self.hideButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
}
// Show feedback
if (!skipFeedback) {
this.setFeedback(getFeedbackText(score, max), score, max, params.UI.scoreBarLabel);
}
self.trigger('resize');
};
/**
* Disables choosing new input.
*/
var disableInput = function () {
$('.h5p-answer', $myDom).attr({
'aria-disabled': 'true',
'tabindex': '-1'
});
};
/**
* Enables new input.
*/
var enableInput = function () {
$('.h5p-answer', $myDom).attr('aria-disabled', 'false');
};
var calcScore = function () {
score = 0;
for (const answer of params.userAnswers) {
const choice = params.answers[answer];
const weight = (choice.weight !== undefined ? choice.weight : 1);
if (choice.correct) {
score += weight;
}
else {
score -= weight;
}
}
if (score < 0) {
score = 0;
}
if (!params.userAnswers.length && blankIsCorrect) {
score = params.weight;
}
if (params.behaviour.singlePoint) {
score = (100 * score / calculateMaxScore()) >= params.behaviour.passPercentage ? params.weight : 0;
}
};
/**
* Removes selections from task.
*/
var removeSelections = function () {
var $answers = $('.h5p-answer', $myDom)
.removeClass('h5p-selected')
.attr('aria-checked', 'false');
if (!params.behaviour.singleAnswer) {
$answers.attr('tabindex', '0');
}
else {
$answers.first().attr('tabindex', '0');
}
// Set focus to first option
$answers.first().focus();
calcScore();
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
this.getXAPIData = function(){
var xAPIEvent = this.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
};
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
definition.description = {
// Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag.
'en-US': $('
' + params.question + '
').text()
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'choice';
definition.correctResponsesPattern = [];
definition.choices = [];
for (var i = 0; i < params.answers.length; i++) {
definition.choices[i] = {
'id': params.answers[i].originalOrder + '',
'description': {
// Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag.
'en-US': $('
' + params.answers[i].text + '
').text()
}
};
if (params.answers[i].correct) {
if (!params.singleAnswer) {
if (definition.correctResponsesPattern.length) {
definition.correctResponsesPattern[0] += '[,]';
// This looks insane, but it's how you separate multiple answers
// that must all be chosen to achieve perfect score...
}
else {
definition.correctResponsesPattern.push('');
}
definition.correctResponsesPattern[0] += params.answers[i].originalOrder;
}
else {
definition.correctResponsesPattern.push('' + params.answers[i].originalOrder);
}
}
}
};
/**
* Add the response part to an xAPI event
*
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
var addResponseToXAPI = function (xAPIEvent) {
var maxScore = self.getMaxScore();
var success = (100 * score / maxScore) >= params.behaviour.passPercentage;
xAPIEvent.setScoredResult(score, maxScore, self, true, success);
if (params.userAnswers === undefined) {
calcScore();
}
// Add the response
var response = '';
for (var i = 0; i < params.userAnswers.length; i++) {
if (response !== '') {
response += '[,]';
}
response += idMap === undefined ? params.userAnswers[i] : idMap[params.userAnswers[i]];
}
xAPIEvent.data.statement.result.response = response;
};
/**
* Create a map pointing from original answers to shuffled answers
*
* @return {number[]} map pointing from original answers to shuffled answers
*/
var getShuffleMap = function() {
params.answers = H5P.shuffleArray(params.answers);
// Create a map from the new id to the old one
var idMap = [];
for (i = 0; i < params.answers.length; i++) {
idMap[i] = params.answers[i].originalOrder;
}
return idMap;
};
// Initialization code
// Randomize order, if requested
var idMap;
// Store original order in answers
for (i = 0; i < params.answers.length; i++) {
params.answers[i].originalOrder = i;
}
if (params.behaviour.randomAnswers) {
idMap = getShuffleMap();
}
// Start with an empty set of user answers.
params.userAnswers = [];
// Restore previous state
if (contentData && contentData.previousState !== undefined) {
// Restore answers
if (contentData.previousState.answers) {
if (!idMap) {
params.userAnswers = contentData.previousState.answers;
}
else {
// The answers have been shuffled, and we must use the id mapping.
for (i = 0; i < contentData.previousState.answers.length; i++) {
for (var k = 0; k < idMap.length; k++) {
if (idMap[k] === contentData.previousState.answers[i]) {
params.userAnswers.push(k);
}
}
}
}
calcScore();
}
}
var hasCheckedAnswer = false;
// Loop through choices
for (var j = 0; j < params.answers.length; j++) {
var ans = params.answers[j];
if (!params.behaviour.singleAnswer) {
// Set role
ans.role = 'checkbox';
ans.tabindex = '0';
if (params.userAnswers.indexOf(j) !== -1) {
ans.checked = 'true';
hasCheckedAnswer = true;
}
}
else {
// Set role
ans.role = 'radio';
// Determine tabindex, checked and extra classes
if (params.userAnswers.length === 0) {
// No correct answers
if (i === 0 || i === params.answers.length) {
ans.tabindex = '0';
}
}
else if (params.userAnswers.indexOf(j) !== -1) {
// This is the correct choice
ans.tabindex = '0';
ans.checked = 'true';
hasCheckedAnswer = true;
}
}
// Set default
if (ans.tabindex === undefined) {
ans.tabindex = '-1';
}
if (ans.checked === undefined) {
ans.checked = 'false';
}
}
H5P.MultiChoice.counter = (H5P.MultiChoice.counter === undefined ? 0 : H5P.MultiChoice.counter + 1);
params.role = (params.behaviour.singleAnswer ? 'radiogroup' : 'group');
params.label = 'h5p-mcq' + H5P.MultiChoice.counter;
/**
* Pack the current state of the interactivity into a object that can be
* serialized.
*
* @public
*/
this.getCurrentState = function () {
var state = {};
if (!idMap) {
state.answers = params.userAnswers;
}
else {
// The answers have been shuffled and must be mapped back to their
// original ID.
state.answers = [];
for (var i = 0; i < params.userAnswers.length; i++) {
state.answers.push(idMap[params.userAnswers[i]]);
}
}
return state;
};
/**
* Check if user has given an answer.
*
* @param {boolean} [ignoreCheck] Ignore returning true from pressing "check-answer" button.
* @return {boolean} True if answer is given
*/
this.getAnswerGiven = function (ignoreCheck) {
var answered = ignoreCheck ? false : this.answered;
return answered || params.userAnswers.length > 0 || blankIsCorrect;
};
this.getScore = function () {
return score;
};
this.getTitle = function () {
return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Multiple Choice');
};
};
H5P.MultiChoice.prototype = Object.create(H5P.Question.prototype);
H5P.MultiChoice.prototype.constructor = H5P.MultiChoice;
;
H5P.Column = (function (EventDispatcher) {
/**
* Column Constructor
*
* @class
* @param {Object} params Describes task behavior
* @param {number} id Content identifier
* @param {Object} data User specific data to adapt behavior
*/
function Column(params, id, data) {
/** @alias H5P.Column# */
var self = this;
// We support events by extending this class
EventDispatcher.call(self);
// Add defaults
params = params || {};
if (params.useSeparators === undefined) {
params.useSeparators = true;
}
this.contentData = data;
// Column wrapper element
var wrapper;
// H5P content in the column
var instances = [];
var instanceContainers = [];
// Number of tasks among instances
var numTasks = 0;
// Number of tasks that has been completed
var numTasksCompleted = 0;
// Keep track of result for each task
var tasksResultEvent = [];
// Keep track of last content's margin state
var previousHasMargin;
/**
* Calculate score and trigger completed event.
*
* @private
*/
var completed = function () {
// Sum all scores
var raw = 0;
var max = 0;
for (var i = 0; i < tasksResultEvent.length; i++) {
var event = tasksResultEvent[i];
raw += event.getScore();
max += event.getMaxScore();
}
self.triggerXAPIScored(raw, max, 'completed');
};
/**
* Generates an event handler for the given task index.
*
* @private
* @param {number} taskIndex
* @return {function} xAPI event handler
*/
var trackScoring = function (taskIndex) {
return function (event) {
if (event.getScore() === null) {
return; // Skip, not relevant
}
if (tasksResultEvent[taskIndex] === undefined) {
// Update number of completed tasks
numTasksCompleted++;
}
// Keep track of latest event with result
tasksResultEvent[taskIndex] = event;
// Track progress
var progressed = self.createXAPIEventTemplate('progressed');
progressed.data.statement.object.definition.extensions['http://id.tincanapi.com/extension/ending-point'] = taskIndex + 1;
self.trigger(progressed);
// Check to see if we're done
if (numTasksCompleted === numTasks) {
// Run this after the current event is sent
setTimeout(function () {
completed(); // Done
}, 0);
}
};
};
/**
* Creates a new ontent instance from the given content parameters and
* then attaches it the wrapper. Sets up event listeners.
*
* @private
* @param {Object} content Parameters
* @param {Object} [contentData] Content Data
*/
var addRunnable = function (content, contentData) {
// Create container for content
var container = document.createElement('div');
container.classList.add('h5p-column-content');
// Content overrides
var library = content.library.split(' ')[0];
if (library === 'H5P.Video') {
// Prevent video from growing endlessly since height is unlimited.
content.params.visuals.fit = false;
}
// Create content instance
var instance = H5P.newRunnable(content, id, undefined, true, contentData);
// Bubble resize events
bubbleUp(instance, 'resize', self);
// Check if instance is a task
if (Column.isTask(instance)) {
// Tasks requires completion
instance.on('xAPI', trackScoring(numTasks));
numTasks++;
}
if (library === 'H5P.Image' || library === 'H5P.TwitterUserFeed') {
// Resize when images are loaded
instance.on('loaded', function () {
self.trigger('resize');
});
}
// Keep track of all instances
instances.push(instance);
instanceContainers.push({
hasAttached: false,
container: container,
instanceIndex: instances.length - 1,
});
// Add to DOM wrapper
wrapper.appendChild(container);
};
/**
* Help get data for content at given index
*
* @private
* @param {number} index
* @returns {Object} Data object with previous state
*/
var grabContentData = function (index) {
var contentData = {
parent: self
};
if (data.previousState && data.previousState.instances && data.previousState.instances[index]) {
contentData.previousState = data.previousState.instances[index];
}
return contentData;
};
/**
* Adds separator before the next content.
*
* @private
* @param {string} libraryName Name of the next content type
* @param {string} useSeparator
*/
var addSeparator = function (libraryName, useSeparator) {
// Determine separator spacing
var thisHasMargin = (hasMargins.indexOf(libraryName) !== -1);
// Only add if previous content exists
if (previousHasMargin !== undefined) {
// Create separator element
var separator = document.createElement('div');
//separator.classList.add('h5p-column-ruler');
// If no margins, check for top margin only
if (!thisHasMargin && (hasTopMargins.indexOf(libraryName) === -1)) {
if (!previousHasMargin) {
// None of them have margin
// Only add separator if forced
if (useSeparator === 'enabled') {
// Add ruler
separator.classList.add('h5p-column-ruler');
// Add space both before and after the ruler
separator.classList.add('h5p-column-space-before-n-after');
}
else {
// Default is to separte using a single space, no ruler
separator.classList.add('h5p-column-space-before');
}
}
else {
// We don't have any margin but the previous content does
// Only add separator if forced
if (useSeparator === 'enabled') {
// Add ruler
separator.classList.add('h5p-column-ruler');
// Add space after the ruler
separator.classList.add('h5p-column-space-after');
}
}
}
else if (!previousHasMargin) {
// We have margin but not the previous content doesn't
// Only add separator if forced
if (useSeparator === 'enabled') {
// Add ruler
separator.classList.add('h5p-column-ruler');
// Add space after the ruler
separator.classList.add('h5p-column-space-before');
}
}
else {
// Both already have margin
if (useSeparator !== 'disabled') {
// Default is to add ruler unless its disabled
separator.classList.add('h5p-column-ruler');
}
}
// Insert into DOM
wrapper.appendChild(separator);
}
// Keep track of spacing for next separator
previousHasMargin = thisHasMargin || (hasBottomMargins.indexOf(libraryName) !== -1);
};
/**
* Creates a wrapper and the column content the first time the column
* is attached to the DOM.
*
* @private
*/
var createHTML = function () {
// Create wrapper
wrapper = document.createElement('div');
// Go though all contents
for (var i = 0; i < params.content.length; i++) {
var content = params.content[i];
// In case the author has created an element without selecting any
// library
if (content.content === undefined) {
continue;
}
if (params.useSeparators) { // (check for global override)
// Add separator between contents
addSeparator(content.content.library.split(' ')[0], content.useSeparator);
}
// Add content
addRunnable(content.content, grabContentData(i));
}
};
/**
* Attach the column to the given container
*
* @param {H5P.jQuery} $container
*/
self.attach = function ($container) {
if (wrapper === undefined) {
// Create wrapper and content
createHTML();
}
// Attach instances that have not been attached
instanceContainers.filter(function (container) { return !container.hasAttached })
.forEach(function (container) {
instances[container.instanceIndex]
.attach(H5P.jQuery(container.container));
// Remove any fullscreen buttons
disableFullscreen(instances[container.instanceIndex]);
});
// Add to DOM
$container.addClass('h5p-column').html('').append(wrapper);
};
/**
* Create object containing information about the current state
* of this content.
*
* @return {Object}
*/
self.getCurrentState = function () {
// Get previous state object or create new state object
var state = (data.previousState ? data.previousState : {});
if (!state.instances) {
state.instances = [];
}
// Grab the current state for each instance
for (var i = 0; i < instances.length; i++) {
var instance = instances[i];
if (instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function') {
state.instances[i] = instance.getCurrentState();
}
}
// Done
return state;
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
self.getXAPIData = function () {
var xAPIEvent = self.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
xAPIEvent.setScoredResult(self.getScore(),
self.getMaxScore(),
self,
true,
self.getScore() === self.getMaxScore()
);
return {
statement: xAPIEvent.data.statement,
children: getXAPIDataFromChildren(instances)
};
};
/**
* Get score for all children
* Contract used for getting the complete score of task.
*
* @return {number} Score for questions
*/
self.getScore = function () {
return instances.reduce(function (prev, instance) {
return prev + (instance.getScore ? instance.getScore() : 0);
}, 0);
};
/**
* Get maximum score possible for all children instances
* Contract.
*
* @return {number} Maximum score for questions
*/
self.getMaxScore = function () {
return instances.reduce(function (prev, instance) {
return prev + (instance.getMaxScore ? instance.getMaxScore() : 0);
}, 0);
};
/**
* Get answer given
* Contract.
*
* @return {boolean} True, if all answers have been given.
*/
self.getAnswerGiven = function () {
return instances.reduce(function (prev, instance) {
return prev && (instance.getAnswerGiven ? instance.getAnswerGiven() : prev);
}, true);
};
/**
* Show solutions.
* Contract.
*/
self.showSolutions = function () {
instances.forEach(function (instance) {
if (instance.toggleReadSpeaker) {
instance.toggleReadSpeaker(true);
}
if (instance.showSolutions) {
instance.showSolutions();
}
if (instance.toggleReadSpeaker) {
instance.toggleReadSpeaker(false);
}
});
};
/**
* Reset task.
* Contract.
*/
self.resetTask = function () {
instances.forEach(function (instance) {
if (instance.resetTask) {
instance.resetTask();
}
});
};
/**
* Get instances for all children
* TODO: This is not a good interface, we should provide handling needed
* handling of the tasks instead of repeating them for each parent...
*
* @return {Object[]} array of instances
*/
self.getInstances = function () {
return instances;
};
/**
* Get title, e.g. for xAPI when Column is subcontent.
*
* @return {string} Title.
*/
self.getTitle = function () {
return H5P.createTitle((self.contentData && self.contentData.metadata && self.contentData.metadata.title) ? self.contentData.metadata.title : 'Column');
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
H5P.jQuery.extend(definition, getxAPIDefinition());
};
/**
* Generate xAPI object definition used in xAPI statements.
* @return {Object}
*/
var getxAPIDefinition = function () {
var definition = {};
definition.interactionType = 'compound';
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.description = {
'en-US': ''
};
return definition;
};
/**
* Get xAPI data from sub content types
*
* @param {Array} of H5P instances
* @returns {Array} of xAPI data objects used to build a report
*/
var getXAPIDataFromChildren = function (children) {
return children.map(function (child) {
if (typeof child.getXAPIData == 'function') {
return child.getXAPIData();
}
}).filter(function (data) {
return !!data;
});
};
// Resize children to fit inside parent
bubbleDown(self, 'resize', instances);
if (wrapper === undefined) {
// Create wrapper and content
createHTML();
}
self.setActivityStarted();
}
Column.prototype = Object.create(EventDispatcher.prototype);
Column.prototype.constructor = Column;
/**
* Makes it easy to bubble events from parent to children
*
* @private
* @param {Object} origin Origin of the Event
* @param {string} eventName Name of the Event
* @param {Array} targets Targets to trigger event on
*/
function bubbleDown(origin, eventName, targets) {
origin.on(eventName, function (event) {
if (origin.bubblingUpwards) {
return; // Prevent send event back down.
}
for (var i = 0; i < targets.length; i++) {
targets[i].trigger(eventName, event);
}
});
}
/**
* Makes it easy to bubble events from child to parent
*
* @private
* @param {Object} origin Origin of the Event
* @param {string} eventName Name of the Event
* @param {Object} target Target to trigger event on
*/
function bubbleUp(origin, eventName, target) {
origin.on(eventName, function (event) {
// Prevent target from sending event back down
target.bubblingUpwards = true;
// Trigger event
target.trigger(eventName, event);
// Reset
target.bubblingUpwards = false;
});
}
/**
* Definition of which content types are tasks
*/
var isTasks = [
'H5P.ImageHotspotQuestion',
'H5P.Blanks',
'H5P.Essay',
'H5P.SingleChoiceSet',
'H5P.MultiChoice',
'H5P.TrueFalse',
'H5P.DragQuestion',
'H5P.Summary',
'H5P.DragText',
'H5P.MarkTheWords',
'H5P.MemoryGame',
'H5P.QuestionSet',
'H5P.InteractiveVideo',
'H5P.CoursePresentation',
'H5P.DocumentationTool'
];
/**
* Check if the given content instance is a task (will give a score)
*
* @param {Object} instance
* @return {boolean}
*/
Column.isTask = function (instance) {
if (instance.isTask !== undefined) {
return instance.isTask; // Content will determine self if it's a task
}
// Go through the valid task names
for (var i = 0; i < isTasks.length; i++) {
// Check against library info. (instanceof is broken in H5P.newRunnable)
if (instance.libraryInfo.machineName === isTasks[i]) {
return true;
}
}
return false;
}
/**
* Definition of which content type have margins
*/
var hasMargins = [
'H5P.AdvancedText',
'H5P.AudioRecorder',
'H5P.Essay',
'H5P.Link',
'H5P.Accordion',
'H5P.Table',
'H5P.GuessTheAnswer',
'H5P.Blanks',
'H5P.MultiChoice',
'H5P.TrueFalse',
'H5P.DragQuestion',
'H5P.Summary',
'H5P.DragText',
'H5P.MarkTheWords',
'H5P.ImageHotspotQuestion',
'H5P.MemoryGame',
'H5P.Dialogcards',
'H5P.QuestionSet',
'H5P.DocumentationTool'
];
/**
* Definition of which content type have top margins
*/
var hasTopMargins = [
'H5P.SingleChoiceSet'
];
/**
* Definition of which content type have bottom margins
*/
var hasBottomMargins = [
'H5P.CoursePresentation',
'H5P.Dialogcards',
'H5P.GuessTheAnswer',
'H5P.ImageSlider'
];
/**
* Remove custom fullscreen buttons from sub content.
* (A bit of a hack, there should have been some sort of overrideā¦)
*
* @param {Object} instance
*/
function disableFullscreen(instance) {
switch (instance.libraryInfo.machineName) {
case 'H5P.CoursePresentation':
if (instance.$fullScreenButton) {
instance.$fullScreenButton.remove();
}
break;
case 'H5P.InteractiveVideo':
instance.on('controls', function () {
if (instance.controls.$fullscreen) {
instance.controls.$fullscreen.remove();
}
});
break;
}
}
return Column;
})(H5P.EventDispatcher);
;