', {
'class': 'h5p-true-false-answer',
role: 'radio',
'aria-checked': false,
html: text + '
',
tabindex: 0, // Tabable by default
click: function (event) {
// Handle left mouse (or tap on touch devices)
if (event.which === 1) {
self.check();
}
},
keydown: function (event) {
if (!enabled) {
return;
}
if ([Keys.SPACE, Keys.ENTER].indexOf(event.keyCode) !== -1) {
self.check();
}
else if ([Keys.LEFT_ARROW, Keys.UP_ARROW, Keys.RIGHT_ARROW, Keys.DOWN_ARROW].indexOf(event.keyCode) !== -1) {
self.uncheck();
self.trigger('invert');
}
},
focus: function () {
self.trigger('focus');
},
blur: function () {
self.trigger('blur');
}
});
var $ariaLabel = $answer.find('.aria-label');
// A bug in Chrome 54 makes the :after icons (V and X) not beeing rendered.
// Doing this in a timeout solves this
// Might be removed when Chrome 56 is out
var chromeBugFixer = function (callback) {
setTimeout(function () {
callback();
}, 0);
};
/**
* Return the dom element representing the alternative
*
* @public
* @method getDomElement
* @return {H5P.jQuery}
*/
self.getDomElement = function () {
return $answer;
};
/**
* Unchecks the alternative
*
* @public
* @method uncheck
* @return {H5P.TrueFalse.Answer}
*/
self.uncheck = function () {
if (enabled) {
$answer.blur();
checked = false;
chromeBugFixer(function () {
$answer.attr('aria-checked', checked);
});
}
return self;
};
/**
* Set tabable or not
* @method tabable
* @param {Boolean} enabled
* @return {H5P.TrueFalse.Answer}
*/
self.tabable = function (enabled) {
$answer.attr('tabIndex', enabled ? 0 : null);
return self;
};
/**
* Checks the alternative
*
* @method check
* @return {H5P.TrueFalse.Answer}
*/
self.check = function () {
if (enabled) {
checked = true;
chromeBugFixer(function () {
$answer.attr('aria-checked', checked);
});
self.trigger('checked');
$answer.focus();
}
return self;
};
/**
* Is this alternative checked?
*
* @method isChecked
* @return {boolean}
*/
self.isChecked = function () {
return checked;
};
/**
* Enable alternative
*
* @method enable
* @return {H5P.TrueFalse.Answer}
*/
self.enable = function () {
$answer.attr({
'aria-disabled': '',
tabIndex: 0
});
enabled = true;
return self;
};
/**
* Disables alternative
*
* @method disable
* @return {H5P.TrueFalse.Answer}
*/
self.disable = function () {
$answer.attr({
'aria-disabled': true,
tabIndex: null
});
enabled = false;
return self;
};
/**
* Reset alternative
*
* @method reset
* @return {H5P.TrueFalse.Answer}
*/
self.reset = function () {
self.enable();
self.uncheck();
self.unmark();
$ariaLabel.html('');
return self;
};
/**
* Marks this alternative as the wrong one
*
* @method markWrong
* @return {H5P.TrueFalse.Answer}
*/
self.markWrong = function () {
chromeBugFixer(function () {
$answer.addClass('wrong');
});
$ariaLabel.html('.' + wrongMessage);
return self;
};
/**
* Marks this alternative as the wrong one
*
* @method markCorrect
* @return {H5P.TrueFalse.Answer}
*/
self.markCorrect = function () {
chromeBugFixer(function () {
$answer.addClass('correct');
});
$ariaLabel.html('.' + correctMessage);
return self;
};
self.unmark = function () {
chromeBugFixer(function () {
$answer.removeClass('wrong correct');
});
return self;
};
}
// Inheritance
Answer.prototype = Object.create(EventDispatcher.prototype);
Answer.prototype.constructor = Answer;
return Answer;
})(H5P.jQuery, H5P.EventDispatcher);
;
var H5P = H5P || {};
/**
* Constructor.
*
* @param {Object} params Options for this library.
* @param {Number} id Content identifier
* @returns {undefined}
*/
(function ($) {
H5P.Image = function (params, id) {
H5P.EventDispatcher.call(this);
if (params.file === undefined || !(params.file instanceof Object)) {
this.placeholder = true;
}
else {
this.source = H5P.getPath(params.file.path, id);
this.width = params.file.width;
this.height = params.file.height;
// Use new copyright information if available. Fallback to old.
if (params.file.copyright !== undefined) {
this.copyright = params.file.copyright;
}
else if (params.copyright !== undefined) {
this.copyright = params.copyright;
}
}
this.alt = params.alt !== undefined ? params.alt : 'New image';
if (params.title !== undefined) {
this.title = params.title;
}
};
H5P.Image.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.Image.prototype.constructor = H5P.Image;
/**
* Wipe out the content of the wrapper and put our HTML in it.
*
* @param {jQuery} $wrapper
* @returns {undefined}
*/
H5P.Image.prototype.attach = function ($wrapper) {
var self = this;
var source = this.source;
if (self.$img === undefined) {
if(self.placeholder) {
self.$img = $('
', {
width: '100%',
height: '100%',
class: 'h5p-placeholder',
title: this.title === undefined ? '' : this.title,
load: function () {
self.trigger('loaded');
}
});
} else {
self.$img = $('
![]()
', {
width: '100%',
height: '100%',
src: source,
alt: this.alt,
title: this.title === undefined ? '' : this.title,
load: function () {
self.trigger('loaded');
}
});
}
}
$wrapper.addClass('h5p-image').html(self.$img);
};
/**
* Gather copyright information for the current content.
*
* @returns {H5P.ContentCopyright}
*/
H5P.Image.prototype.getCopyrights = function () {
if (this.copyright === undefined) {
return;
}
var info = new H5P.ContentCopyrights();
var image = new H5P.MediaCopyright(this.copyright);
image.setThumbnail(new H5P.Thumbnail(this.source, this.width, this.height));
info.addMedia(image);
return info;
};
return H5P.Image;
}(H5P.jQuery));
;
var H5P = H5P || {};
/**
* DragQuestion module.
*
* @param {jQuery} $
*/
H5P.DragQuestion = (function ($) {
/**
* Initialize module.
*
* @class
* @extend H5P.Question
* @param {Object} options Run parameters
* @param {Number} id Content identification
*/
function C(options, contentId, contentData) {
var self = this;
var i, j;
this.id = this.contentId = contentId;
H5P.Question.call(self, 'dragquestion');
this.options = $.extend(true, {}, {
scoreShow: 'Check',
correct: 'Show solution',
tryAgain: 'Retry',
feedback: 'You got @score of @total points',
question: {
settings: {
questionTitle: 'Drag and drop',
showTitle: true,
size: {
width: 620,
height: 310
},
dropZoneHighlighting: 'dragging',
autoAlignSpacing: 2
},
task: {
elements: [],
dropZones: []
}
},
behaviour: {
enableRetry: true,
preventResize: false,
singlePoint: true,
showSolutionsRequiresInput: true,
applyPenalties: true
}
}, options);
this.draggables = [];
this.dropZones = [];
this.answered = (contentData && contentData.previousState !== undefined && contentData.previousState.answers !== undefined && contentData.previousState.answers.length);
this.blankIsCorrect = true;
this.backgroundOpacity = (this.options.backgroundOpacity === undefined || this.options.backgroundOpacity.trim() === '') ? undefined : this.options.backgroundOpacity;
// List of drop zones that has no elements, i.e. not used for the task
var dropZonesWithoutElements = [];
// Create map over correct drop zones for elements
var task = this.options.question.task;
this.correctDZs = [];
for (i = 0; i < task.dropZones.length; i++) {
dropZonesWithoutElements.push(true); // All true by default
var correctElements = task.dropZones[i].correctElements;
for (j = 0; j < correctElements.length; j++) {
var correctElement = correctElements[j];
if (this.correctDZs[correctElement] === undefined) {
this.correctDZs[correctElement] = [];
}
this.correctDZs[correctElement].push(i);
}
}
this.weight = 1;
// Add draggable elements
for (i = 0; i < task.elements.length; i++) {
var element = task.elements[i];
if (element.dropZones === undefined || !element.dropZones.length) {
continue; // Not a draggable
}
if (this.backgroundOpacity !== undefined) {
element.backgroundOpacity = this.backgroundOpacity;
}
// Restore answers from last session
var answers = null;
if (contentData && contentData.previousState !== undefined && contentData.previousState.answers !== undefined && contentData.previousState.answers[i] !== undefined) {
answers = contentData.previousState.answers[i];
}
// Create new draggable instance
var draggable = new Draggable(element, i, answers);
if (self.options.question.settings.dropZoneHighlighting === 'dragging') {
draggable.on('drag', function () {
self.$container.addClass('h5p-dq-highlight-dz');
});
draggable.on('dropped', function (event) {
self.$container.removeClass('h5p-dq-highlight-dz');
});
}
draggable.on('interacted', function () {
self.answered = true;
self.triggerXAPIScored(self.getScore(), self.getMaxScore(), 'interacted');
});
draggable.on('leavingDropZone', function (event) {
self.dropZones[event.data.dropZone].removeAlignable(event.data.$);
});
this.draggables[i] = draggable;
for (j = 0; j < element.dropZones.length; j++) {
dropZonesWithoutElements[element.dropZones[j]] = false;
}
}
// Create a count to subtrack from score
this.numDropZonesWithoutElements = 0;
// Add drop zones
for (i = 0; i < task.dropZones.length; i++) {
var dropZone = task.dropZones[i];
if (dropZonesWithoutElements[i] === true) {
this.numDropZonesWithoutElements += 1;
}
if (this.blankIsCorrect && dropZone.correctElements.length) {
this.blankIsCorrect = false;
}
if (dropZone.autoAlign) {
dropZone.autoAlign = {
spacing: self.options.question.settings.autoAlignSpacing,
size: self.options.question.settings.size
};
}
this.dropZones[i] = new DropZone(dropZone, i);
}
this.on('resize', self.resize, self);
this.on('domChanged', function(event) {
if (self.contentId === event.data.contentId) {
self.trigger('resize');
}
});
this.on('enterFullScreen', function () {
if (self.$container) {
self.$container.parents('.h5p-content').css('height', '100%');
self.trigger('resize');
}
});
this.on('exitFullScreen', function () {
if (self.$container) {
self.$container.parents('.h5p-content').css('height', 'auto');
self.trigger('resize');
}
});
}
C.prototype = Object.create(H5P.Question.prototype);
C.prototype.constructor = C;
/**
* Registers this question type's DOM elements before they are attached.
* Called from H5P.Question.
*/
C.prototype.registerDomElements = function () {
var self = this;
// Register introduction section
if (self.options.question.settings.showTitle) {
self.setIntroduction('
' + self.options.question.settings.questionTitle + '
');
}
// Set class if no background
var classes = '';
if (this.options.question.settings.background !== undefined) {
classes += 'h5p-dragquestion-has-no-background';
}
if (self.options.question.settings.dropZoneHighlighting === 'always' ) {
if (classes) {
classes += ' ';
}
classes += 'h5p-dq-highlight-dz-always';
}
// Register task content area
self.setContent(self.createQuestionContent(), {
'class': classes
});
// ... and buttons
self.registerButtons();
setTimeout(function () {
self.trigger('resize');
}, 200);
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*
* @return {Object} xAPI data
*/
C.prototype.getXAPIData = function () {
var xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
}
};
/**
* Add the question itselt to the definition part of an xAPIEvent
*/
C.prototype.addQuestionToXAPI = function(xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(definition, this.getXAPIDefinition());
};
/**
* Get object definition for xAPI statement.
*
* @return {Object} xAPI object definition
*/
C.prototype.getXAPIDefinition = function () {
var 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': $('
' + this.options.question.settings.questionTitle + '
').text()
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'matching';
// Add sources, i.e. draggables
definition.source = [];
for (var i = 0; i < this.options.question.task.elements.length; i++) {
var el = this.options.question.task.elements[i];
if (el.dropZones && el.dropZones.length) {
var desc = el.type.params.alt ? el.type.params.alt : el.type.params.text;
definition.source.push({
'id': '' + i,
'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': $('
' + desc + '
').text()
}
});
}
}
// Add targets, i.e. drop zones, and the correct response pattern.
definition.correctResponsesPattern = [''];
definition.target = [];
var firstCorrectPair = true;
for (var i = 0; i < this.options.question.task.dropZones.length; i++) {
definition.target.push({
'id': '' + i,
'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': $('
' + this.options.question.task.dropZones[i].label + '
').text()
}
});
if (this.options.question.task.dropZones[i].correctElements) {
for (var j = 0; j < this.options.question.task.dropZones[i].correctElements.length; j++) {
if (!firstCorrectPair) {
definition.correctResponsesPattern[0] += '[,]';
}
definition.correctResponsesPattern[0] += i + '[.]' + this.options.question.task.dropZones[i].correctElements[j];
firstCorrectPair = false;
}
}
}
return definition;
};
/**
* Add the response part to an xAPI event
*
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
C.prototype.addResponseToXAPI = function(xAPIEvent) {
var maxScore = this.getMaxScore();
var score = this.getScore();
var success = score == maxScore ? true : false;
xAPIEvent.setScoredResult(score, maxScore, this, true, success);
xAPIEvent.data.statement.result.response = this.getUserXAPIResponse();
};
/**
* Get what the user has answered encoded as an xAPI response pattern
*
* @return {string} xAPI encoded user response pattern
*/
C.prototype.getUserXAPIResponse = function () {
var answers = this.getUserAnswers();
if (!answers) {
return response;
}
return answers
.filter(function (answerMapping) {
return answerMapping.elements.length;
})
.map(function (answerMapping, index) {
return answerMapping.elements
.filter(function (element) {
return element.dropZone !== undefined;
}).map(function (element) {
return element.dropZone + '[.]' + index;
}).join('[,]');
}).filter(function (pattern) {
return pattern !== undefined && pattern !== '';
}).join('[,]');
};
/**
* Returns user answers
*/
C.prototype.getUserAnswers = function () {
return this.draggables.map(function (draggable, index) {
return {
index: index,
draggable: draggable
};
}).filter(function (draggableMapping) {
return draggableMapping.draggable !== undefined &&
draggableMapping.draggable.elements;
}).map(function (draggableMapping) {
return {
index: draggableMapping.index,
elements: draggableMapping.draggable.elements
}
});
};
/**
* Append field to wrapper.
*/
C.prototype.createQuestionContent = function () {
var i;
// If reattaching, we no longer show solution. So forget that we
// might have done so before.
this.$container = $('
');
if (this.options.question.settings.background !== undefined) {
this.$container.css('backgroundImage', 'url("' + H5P.getPath(this.options.question.settings.background.path, this.id) + '")');
}
var task = this.options.question.task;
// Add elements (static and draggable)
for (i = 0; i < task.elements.length; i++) {
var element = task.elements[i];
if (element.dropZones !== undefined && element.dropZones.length !== 0) {
// Attach draggable elements
this.draggables[i].appendTo(this.$container, this.id);
}
else {
// Add static element
var $element = this.addElement(element, 'static', i);
H5P.newRunnable(element.type, this.id, $element);
var timedOutOpacity = function($el, el) {
setTimeout(function () {
C.setOpacity($el, 'background', el.backgroundOpacity);
}, 0);
};
timedOutOpacity($element, element);
}
}
// Attach drop zones
for (i = 0; i < this.dropZones.length; i++) {
this.dropZones[i].appendTo(this.$container, this.draggables);
}
return this.$container;
};
C.prototype.registerButtons = function () {
// Add show score button
this.addSolutionButton();
this.addRetryButton();
};
/**
* Makes sure element gets correct opacity when hovered.
*
* @param {jQuery} $element
* @param {Object} element
*/
C.addHover = function ($element, backgroundOpacity) {
$element.hover(function () {
$element.addClass('h5p-draggable-hover');
if (!$element.parent().hasClass('h5p-dragging')) {
C.setElementOpacity($element, backgroundOpacity);
}
}, function () {
if (!$element.parent().hasClass('h5p-dragging')) {
setTimeout(function () {
$element.removeClass('h5p-draggable-hover');
C.setElementOpacity($element, backgroundOpacity);
}, 1);
}
});
C.setElementOpacity($element, backgroundOpacity);
};
/**
* Makes element background transparent.
*
* @param {jQuery} $element
* @param {Number} opacity
*/
C.setElementOpacity = function ($element, opacity) {
C.setOpacity($element, 'borderColor', opacity);
C.setOpacity($element, 'boxShadow', opacity);
C.setOpacity($element, 'background', opacity);
};
/**
* Add solution button to our container.
*/
C.prototype.addSolutionButton = function () {
var that = this;
this.addButton('check-answer', this.options.scoreShow, function () {
that.answered = true;
that.showAllSolutions();
that.showScore();
var xAPIEvent = that.createXAPIEventTemplate('answered');
that.addQuestionToXAPI(xAPIEvent);
that.addResponseToXAPI(xAPIEvent);
that.trigger(xAPIEvent);
});
};
/**
* Add retry button to our container.
*/
C.prototype.addRetryButton = function () {
var that = this;
this.addButton('try-again', this.options.tryAgain, function () {
that.resetTask();
that.showButton('check-answer');
that.hideButton('try-again');
}, false);
};
/**
* Add element/drop zone to task.
*
* @param {Object} element
* @param {String} type Class
* @param {Number} id
* @returns {jQuery}
*/
C.prototype.addElement = function (element, type, id) {
return $('
').appendTo(this.$container).data('id', id);
};
/**
* Set correct height of container
*/
C.prototype.resize = function (e) {
var self = this;
// Make sure we use all the height we can get. Needed to scale up.
if (this.$container === undefined || !this.$container.is(':visible')) {
// Not yet attached or visible – not possible to resize correctly
return;
}
// Check if decreasing iframe size
var decreaseSize = e && e.data && e.data.decreaseSize;
if (!decreaseSize) {
this.$container.css('height', '99999px');
self.$container.parents('.h5p-standalone.h5p-dragquestion').css('width', '');
}
var size = this.options.question.settings.size;
var ratio = size.width / size.height;
var parentContainer = this.$container.parent();
// Use parent container as basis for resize.
var width = parentContainer.width() - parseFloat(parentContainer.css('margin-left')) - parseFloat(parentContainer.css('margin-right'));
// Check if we need to apply semi full screen fix.
var $semiFullScreen = self.$container.parents('.h5p-standalone.h5p-dragquestion.h5p-semi-fullscreen');
if ($semiFullScreen.length) {
// Reset semi fullscreen width
$semiFullScreen.css('width', '');
// Decrease iframe size
if (!decreaseSize) {
self.$container.css('width', '10px');
$semiFullScreen.css('width', '');
// Trigger changes
setTimeout(function () {
self.trigger('resize', {decreaseSize: true});
}, 200);
}
// Set width equal to iframe parent width, since iframe content has not been update yet.
var $iframe = $(window.frameElement);
if ($iframe) {
var $iframeParent = $iframe.parent();
width = $iframeParent.width();
$semiFullScreen.css('width', width + 'px');
}
}
var height = width / ratio;
// Set natural size if no parent width
if (width <= 0) {
width = size.width;
height = size.height;
}
this.$container.css({
width: width + 'px',
height: height + 'px',
fontSize: (16 * (width / size.width)) + 'px'
});
};
/**
* Get css position in percentage.
*
* @param {jQuery} $container
* @param {jQuery} $element
* @returns {Object} CSS position
*/
C.positionToPercentage = function ($container, $element) {
return {
top: (parseInt($element.css('top')) * 100 / $container.innerHeight()) + '%',
left: (parseInt($element.css('left')) * 100 / $container.innerWidth()) + '%'
};
};
/**
* Disables all draggables.
* @public
*/
C.prototype.disableDraggables = function () {
this.draggables.forEach(function (draggable) {
draggable.disable();
});
};
/**
* Enables all draggables.
* @public
*/
C.prototype.enableDraggables = function () {
this.draggables.forEach(function (draggable) {
draggable.enable();
});
};
/**
* Get amount of empty drop zones.
*
* @param {number} totalDropZones Total drop zones in question
* @param {Array} correctDZs Correct drop zones for draggables
* @return {number} Amount of empty drop zones in question
*/
C.prototype.getDropzoneWithoutAnswer = function (totalDropZones, correctDZs) {
//Index of correctDZs is the draggable, and value is the drop zone it belongs to
var correctDropZones = [];
correctDZs.forEach(function (draggable) {
if (draggable.length) {
draggable.forEach(function (dropZone) {
if (correctDropZones.indexOf(dropZone) < 0) {
correctDropZones.push(dropZone);
}
});
}
});
return totalDropZones - correctDropZones.length - this.numDropZonesWithoutElements;
};
/**
* Shows the correct solutions on the boxes and disables input and buttons depending on settings.
* @public
* @params {Boolean} skipVisuals Skip visual animations.
*/
C.prototype.showAllSolutions = function (skipVisuals) {
this.points = 0;
this.rawPoints = 0;
// One correct point for each "no solution" dropzone
var emptyDropzones = this.getDropzoneWithoutAnswer(this.dropZones.length, this.correctDZs);
this.points += emptyDropzones;
this.rawPoints += emptyDropzones;
for (var i = 0; i < this.draggables.length; i++) {
var draggable = this.draggables[i];
if (draggable === undefined) {
continue;
}
//Disable all draggables in check mode.
if (!skipVisuals) {
draggable.disable();
}
// Find out where we are.
this.points += draggable.results(skipVisuals, this.correctDZs[i]);
this.rawPoints += draggable.rawPoints;
}
if (this.points < 0) {
this.points = 0;
}
if (!this.answered && this.blankIsCorrect) {
this.points = this.weight;
}
if (this.options.behaviour.singlePoint) {
this.points = (this.points === this.calculateMaxScore() ? 1 : 0);
}
if (!skipVisuals) {
this.hideButton('check-answer');
}
if (this.options.behaviour.enableRetry && !skipVisuals) {
this.showButton('try-again');
}
if (this.hasButton('check-answer') && (this.options.behaviour.enableRetry === false || this.points === this.getMaxScore())) {
// Max score reached, or the user cannot try again.
this.hideButton('try-again');
}
};
/**
* Display the correct solutions, hides button and disables input.
* Used in contracts.
* @public
*/
C.prototype.showSolutions = function () {
this.showAllSolutions();
this.showScore();
//Hide solution button:
this.hideButton('check-answer');
this.hideButton('try-again');
//Disable dragging during "solution" mode
this.disableDraggables();
};
/**
* Resets the task.
* Used in contracts.
* @public
*/
C.prototype.resetTask = function () {
this.points = 0;
this.rawPoints = 0;
this.answered = false;
//Enables Draggables
this.enableDraggables();
//Reset position and feedback.
this.draggables.forEach(function (draggable) {
draggable.resetPosition();
});
//Show solution button
this.showButton('check-answer');
this.hideButton('try-again');
this.setFeedback();
};
/**
* Calculates the real max score.
*
* @returns {Number} Max points
*/
C.prototype.calculateMaxScore = function () {
var max = 0;
if (this.blankIsCorrect) {
return this.getDropzoneWithoutAnswer(this.dropZones.length, this.correctDZs);
}
max += this.getDropzoneWithoutAnswer(this.dropZones.length, this.correctDZs);
var elements = this.options.question.task.elements;
for (var i = 0; i < elements.length; i++) {
var correctDropZones = this.correctDZs[i];
if (correctDropZones === undefined || !correctDropZones.length) {
continue;
}
if (elements[i].multiple) {
max += correctDropZones.length;
}
else {
max++;
}
}
return max;
};
/**
* Get maximum score.
*
* @returns {Number} Max points
*/
C.prototype.getMaxScore = function () {
return (this.options.behaviour.singlePoint ? this.weight : this.calculateMaxScore());
};
/**
* Count the number of correct answers.
* Only works while showing solution.
*
* @returns {Number} Points
*/
C.prototype.getScore = function () {
this.showAllSolutions(true);
var actualPoints = (this.options.behaviour.applyPenalties || this.options.behaviour.singlePoint) ? this.points : this.rawPoints;
delete this.points;
delete this.rawPoints;
return actualPoints;
};
/**
* Checks if all has been answered.
*
* @returns {Boolean}
*/
C.prototype.getAnswerGiven = function () {
return !this.options.behaviour.showSolutionsRequiresInput || this.answered || this.blankIsCorrect;
};
/**
* Shows the score to the user when the score button is pressed.
*/
C.prototype.showScore = function () {
var maxScore = this.calculateMaxScore();
if (this.options.behaviour.singlePoint) {
maxScore = 1;
}
var actualPoints = (this.options.behaviour.applyPenalties || this.options.behaviour.singlePoint) ? this.points : this.rawPoints;
var scoreText = this.options.feedback.replace('@score', actualPoints).replace('@total', maxScore);
var helpText = (this.options.behaviour.enableScoreExplanation && this.options.behaviour.applyPenalties) ? this.options.scoreExplanation : false;
this.setFeedback(scoreText, actualPoints, maxScore, undefined, helpText);
};
/**
* Packs info about the current state of the task into a object for
* serialization.
*
* @public
* @returns {object}
*/
C.prototype.getCurrentState = function () {
var state = {answers: []};
for (var i = 0; i < this.draggables.length; i++) {
var draggable = this.draggables[i];
if (draggable === undefined) {
continue;
}
var draggableAnswers = [];
for (var j = 0; j < draggable.elements.length; j++) {
var element = draggable.elements[j];
if (element === undefined || element.dropZone === undefined) {
continue;
}
// Verify that draggables actually has its correct positions
// (could have been chaged by auto-align without being updated in draggable)
element.position = C.positionToPercentage(this.$container, element.$);
// Store position and drop zone.
draggableAnswers.push({
x: Number(element.position.left.replace('%', '')),
y: Number(element.position.top.replace('%', '')),
dz: element.dropZone
});
}
if (draggableAnswers.length) {
// Add answers to state object for storage
state.answers[i] = draggableAnswers;
}
}
return state;
};
/**
* Gather copyright information for the current content.
*
* @returns {H5P.ContentCopyright}
*/
C.prototype.getCopyrights = function () {
var self = this;
var info = new H5P.ContentCopyrights();
var background = self.options.question.settings.background;
if (background !== undefined && background.copyright !== undefined) {
var image = new H5P.MediaCopyright(background.copyright);
image.setThumbnail(new H5P.Thumbnail(H5P.getPath(background.path, self.id), background.width, background.height));
info.addMedia(image);
}
for (var i = 0; i < self.options.question.task.elements.length; i++) {
var element = self.options.question.task.elements[i];
var instance = H5P.newRunnable(element.type, self.id);
if (instance.getCopyrights !== undefined) {
var rights = instance.getCopyrights();
rights.setLabel((element.dropZones.length ? 'Draggable ' : 'Static ') + (element.type.params.contentName !== undefined ? element.type.params.contentName : 'element'));
info.addContent(rights);
}
}
return info;
};
C.prototype.getTitle = function() {
return H5P.createTitle(this.options.question.settings.questionTitle);
};
/**
* Makes element background, border and shadow transparent.
*
* @param {jQuery} $element
* @param {String} property
* @param {Number} opacity
*/
C.setOpacity = function ($element, property, opacity) {
if (property === 'background') {
// Set both color and gradient.
C.setOpacity($element, 'backgroundColor', opacity);
C.setOpacity($element, 'backgroundImage', opacity);
if (!opacity) {
$element.css({
"background-color": "rgba(245, 245, 245, 0)",
"background-image": "none"
});
}
return;
}
opacity = (opacity === undefined ? 1 : opacity / 100);
// Private. Get css properties objects.
function getProperties(property, value) {
switch (property) {
case 'borderColor':
return {
borderTopColor: value,
borderRightColor: value,
borderBottomColor: value,
borderLeftColor: value
};
default:
var properties = {};
properties[property] = value;
return properties;
}
}
var original = $element.css(property);
// Reset css to be sure we're using CSS and not inline values.
var properties = getProperties(property, '');
$element.css(properties);
// Determine prop and assume all props are the same and use the first.
for (var prop in properties) {
break;
}
// Get value from css
var style = $element.css(prop);
if (style === '' || style === 'none') {
// No value from CSS, fall back to original
style = original;
}
style = C.setAlphas(style, 'rgba(', opacity); // Update rgba
style = C.setAlphas(style, 'rgb(', opacity); // Convert rgb
$element.css(getProperties(property, style));
};
/**
* Updates alpha channel for colors in the given style.
*
* @param {String} style
* @param {String} prefix
* @param {Number} alpha
*/
C.setAlphas = function (style, prefix, alpha) {
// Style undefined
if (!style) {
return;
}
var colorStart = style.indexOf(prefix);
while (colorStart !== -1) {
var colorEnd = style.indexOf(')', colorStart);
var channels = style.substring(colorStart + prefix.length, colorEnd).split(',');
// Set alpha channel
channels[3] = (channels[3] !== undefined ? parseFloat(channels[3]) * alpha : alpha);
style = style.substring(0, colorStart) + 'rgba(' + channels.join(',') + style.substring(colorEnd, style.length);
// Look for more colors
colorStart = style.indexOf(prefix, colorEnd);
}
return style;
};
/**
* Creates a new draggable instance.
* Makes it easier to keep track of all instance variables and elements.
*
* @class
* @param {object} element
* @param {number} id
* @param {array} [answers] from last session
*/
function Draggable(element, id, answers) {
var self = this;
H5P.EventDispatcher.call(this);
self.$ = $(self);
self.id = id;
self.elements = [];
self.x = element.x;
self.y = element.y;
self.width = element.width;
self.height = element.height;
self.backgroundOpacity = element.backgroundOpacity;
self.dropZones = element.dropZones;
self.type = element.type;
self.multiple = element.multiple;
if (answers) {
if (self.multiple) {
// Add base element
self.elements.push({});
}
// Add answers
for (var i = 0; i < answers.length; i++) {
self.elements.push({
dropZone: answers[i].dz,
position: {
left: answers[i].x + '%',
top: answers[i].y + '%'
}
});
}
}
}
Draggable.prototype = Object.create(H5P.EventDispatcher.prototype);
Draggable.prototype.constructor = Draggable;
/**
* Insert draggable elements into the given container.
*
* @param {jQuery} $container
* @param {Number} contentId
* @returns {undefined}
*/
Draggable.prototype.appendTo = function ($container, contentId) {
var self = this;
if (!self.elements.length) {
self.attachElement(null, $container, contentId);
}
else {
for (var i = 0; i < self.elements.length; i++) {
self.attachElement(i, $container, contentId);
}
}
};
/**
* Attach the given element to the given container.
*
* @param {Number} index
* @param {jQuery} $container
* @param {Number} contentId
* @returns {undefined}
*/
Draggable.prototype.attachElement = function (index, $container, contentId) {
var self = this;
var element;
if (index === null) {
// Create new element
element = {};
self.elements.push(element);
index = self.elements.length - 1;
}
else {
// Get old element
element = self.elements[index];
}
// Attach element
element.$ = $('
', {
class: 'h5p-draggable',
css: {
left: self.x + '%',
top: self.y + '%',
width: self.width + 'em',
height: self.height + 'em'
},
appendTo: $container
})
.draggable({
revert: function (dropZone) {
$container.removeClass('h5p-dragging');
var $this = $(this);
$this.removeClass('h5p-dropped').data("uiDraggable").originalPosition = {
top: self.y + '%',
left: self.x + '%'
};
C.setElementOpacity($this, self.backgroundOpacity);
self.trigger('dropped');
return !dropZone;
},
start: function(event, ui) {
var $this = $(this);
if (self.multiple && element.dropZone === undefined) {
// Leave a new element for next drag
self.attachElement(null, $container, contentId);
}
// Send element to the top!
$this.removeClass('h5p-wrong').detach().appendTo($container);
$container.addClass('h5p-dragging');
C.setElementOpacity($this, self.backgroundOpacity);
self.trigger('drag');
},
stop: function(event, ui) {
var $this = $(this);
// Convert position to % to support scaling.
element.position = C.positionToPercentage($container, $this);
$this.css(element.position);
var addToZone = $this.data('addToZone');
if (addToZone !== undefined) {
$this.removeData('addToZone');
if (self.multiple) {
// Check that we're the only element here
for (var i = 0; i < self.elements.length; i++) {
if (i !== index && self.elements[i] !== undefined && self.elements[i].dropZone === addToZone) {
// Remove element
if (self.elements[index].dropZone !== undefined && self.elements[index].dropZone !== addToZone) {
// Leaving old drop zone!
self.trigger('leavingDropZone', element);
}
$this.remove();
delete self.elements[index];
return;
}
}
}
if (element.dropZone !== undefined && element.dropZone !== addToZone) {
// Leaving old drop zone!
self.trigger('leavingDropZone', element);
}
element.dropZone = addToZone;
$this.addClass('h5p-dropped');
C.setElementOpacity($this, self.backgroundOpacity);
self.trigger('interacted');
}
else {
if (self.multiple) {
// Remove element
if (self.elements[index].dropZone !== undefined) {
self.trigger('leavingDropZone', self.elements[index]);
}
$this.remove();
delete self.elements[index];
}
else {
// Reset position and drop zone.
if (element.dropZone !== undefined) {
self.trigger('leavingDropZone', element);
delete element.dropZone;
}
delete element.position;
}
}
}
}).css('position', '');
self.element = element;
if (element.position) {
// Restore last position
element.$.css(element.position).addClass('h5p-dropped');
}
C.addHover(element.$, self.backgroundOpacity);
H5P.newRunnable(self.type, contentId, element.$);
// Update opacity when element is attached.
setTimeout(function () {
C.setElementOpacity(element.$, self.backgroundOpacity);
}, 0);
};
/**
* Check if this element can be dragged to the given drop zone.
*
* @param {Number} id
* @returns {Boolean}
*/
Draggable.prototype.hasDropZone = function (id) {
var self = this;
for (var i = 0; i < self.dropZones.length; i++) {
if (parseInt(self.dropZones[i]) === id) {
return true;
}
}
return false;
};
/**
* Resets the position of the draggable to its' original position.
* @public
*/
Draggable.prototype.resetPosition = function () {
var self = this;
this.elements.forEach(function (draggable) {
//If the draggable is in a dropzone reset its' position and feedback.
if (draggable.dropZone !== undefined) {
var element = draggable.$;
//Revert the button to initial position and then remove it.
element.animate({
left: self.x + '%',
top: self.y + '%'
}, function () {
//Remove the draggable if it is an infinity draggable.
if (self.multiple) {
if (element.dropZone !== undefined) {
self.trigger('leavingDropZone', element);
}
element.remove();
//Delete the element from elements list to avoid a cluster of draggables on top of infinity draggable.
if (self.elements.indexOf(draggable) >= 0) {
delete self.elements[self.elements.indexOf(draggable)];
}
}
});
// Reset element style
element.removeClass('h5p-wrong')
.removeClass('h5p-correct')
.removeClass('h5p-dropped')
.css({
border: '',
background: ''
});
C.setElementOpacity(element, self.backgroundOpacity);
}
});
// Draggable removed from dropzone.
if (self.element.dropZone !== undefined) {
self.trigger('leavingDropZone', self.element);
delete self.element.dropZone;
}
// Reset style on initial element
self.element.$.removeClass('h5p-wrong')
.removeClass('h5p-correct')
.removeClass('h5p-dropped')
.css({
border: '',
background: ''
});
C.setElementOpacity(self.element.$, self.backgroundOpacity);
};
/**
* Check if the given draggable dom element is a part of this draggable.
*
* @param {Object} draggable
* @returns {Boolean}
*/
Draggable.prototype.is = function (draggable) {
var self = this;
for (var i = 0; i < self.elements.length; i++) {
if (self.elements[i] !== undefined && self.elements[i].$.is(draggable)) {
return true;
}
}
return false;
};
/**
* Detemine if any of our elements is in the given drop zone.
*
* @param {Number} id
* @returns {Boolean}
*/
Draggable.prototype.isInDropZone = function (id) {
var self = this;
for (var i = 0; i < self.elements.length; i++) {
if (self.elements[i] !== undefined && self.elements[i].dropZone === id) {
return true;
}
}
return false;
};
/**
* Disables the draggable.
* @public
*/
Draggable.prototype.disable = function () {
var self = this;
for (var i = 0; i < self.elements.length; i++) {
if (self.elements[i] !== undefined) {
self.elements[i].$.draggable('disable');
}
}
};
/**
* Enables the draggable.
* @public
*/
Draggable.prototype.enable = function () {
var self = this;
for (var i = 0; i < self.elements.length; i++) {
if (self.elements[i] !== undefined) {
self.elements[i].$.draggable('enable');
}
}
};
/**
* Calculate score for this draggable.
*
* @param {Boolean} skipVisuals
* @param {Array} solutions
* @returns {Number}
*/
Draggable.prototype.results = function (skipVisuals, solutions) {
var self = this;
var i, j, element, dropZone, correct, points = 0;
self.rawPoints = 0;
if (solutions === undefined) {
// We should not be anywhere.
for (i = 0; i < self.elements.length; i++) {
element = self.elements[i];
if (element !== undefined && element.dropZone !== undefined) {
// ... but we are!
if (skipVisuals !== true) {
element.$.addClass('h5p-wrong');
C.setElementOpacity(element.$, self.backgroundOpacity);
}
points--;
}
}
return points;
}
// Are we somewhere we should be?
for (i = 0; i < self.elements.length; i++) {
element = self.elements[i];
if (element === undefined || element.dropZone === undefined) {
continue; // We have not been placed anywhere, we're neither wrong nor correct.
}
correct = false;
for (j = 0; j < solutions.length; j++) {
if (element.dropZone === solutions[j]) {
// Yepp!
if (skipVisuals !== true) {
element.$.addClass('h5p-correct').draggable('disable');
C.setElementOpacity(element.$, self.backgroundOpacity);
}
correct = true;
self.rawPoints++;
points++;
break;
}
}
if (!correct) {
// Nope, we're in another zone
if (skipVisuals !== true) {
element.$.addClass('h5p-wrong');
C.setElementOpacity(element.$, self.backgroundOpacity);
}
points--;
}
}
return points;
};
/**
* Creates a new drop zone instance.
* Makes it easy to keep track of all instance variables.
*
* @param {Object} dropZone
* @param {Number} id
* @returns {_L8.DropZone}
*/
function DropZone(dropZone, id) {
var self = this;
self.id = id;
self.showLabel = dropZone.showLabel;
self.label = dropZone.label;
self.x = dropZone.x;
self.y = dropZone.y;
self.width = dropZone.width;
self.height = dropZone.height;
self.backgroundOpacity = dropZone.backgroundOpacity;
self.tip = dropZone.tip;
self.single = dropZone.single;
self.autoAlignEnabled = dropZone.autoAlign;
self.alignables = [];
}
/**
* Insert drop zone in the given container.
*
* @param {jQuery} $container
* @param {Array} draggables
* @returns {undefined}
*/
DropZone.prototype.appendTo = function ($container, draggables) {
var self = this;
// Prepare inner html
var html = '
';
var extraClass = '';
if (self.showLabel) {
html = '
' + self.label + '
' + html;
extraClass = ' h5p-has-label';
}
// Create drop zone element
self.$dropZone = $('
', {
class: 'h5p-dropzone' + extraClass,
css: {
left: self.x + '%',
top: self.y + '%',
width: self.width + 'em',
height: self.height + 'em'
},
html: html
})
.appendTo($container)
.children('.h5p-inner')
.droppable({
activeClass: 'h5p-active',
tolerance: 'intersect',
accept: function (draggable) {
var element;
for (var i = 0; i < draggables.length; i++) {
if (draggables[i] === undefined) {
continue;
}
if (self.single && draggables[i].isInDropZone(self.id)) {
// This drop zone is already occupied!
return false;
}
if (draggables[i].is(draggable)) {
// Found the draggable's instance
element = draggables[i];
if (!self.single) {
break;
}
}
}
if (element === undefined) {
return;
}
// Check to see if the draggable can be dropped in this zone
return element.hasDropZone(self.id);
},
drop: function (event, ui) {
var $this = $(this);
C.setOpacity($this.removeClass('h5p-over'), 'background', self.backgroundOpacity);
ui.draggable.data('addToZone', self.id);
if (self.autoAlignEnabled) {
if (self.getIndexOf(ui.draggable) === -1) {
// Add to alignables
self.alignables.push(ui.draggable);
}
// Trigger alignment
self.autoAlign();
}
},
over: function (event, ui) {
C.setOpacity($(this).addClass('h5p-over'), 'background', self.backgroundOpacity);
},
out: function (event, ui) {
C.setOpacity($(this).removeClass('h5p-over'), 'background', self.backgroundOpacity);
}
})
.end();
// Add tip after setOpacity(), so this does not get background opacity:
if (self.tip !== undefined && self.tip.trim().length) {
self.$dropZone.append(H5P.JoubelUI.createTip(self.tip));
}
if (self.autoAlignEnabled) {
draggables.forEach(function (draggable) {
var dragEl = draggable.element.$;
// Add to alignables
if (draggable.isInDropZone(self.id) && self.getIndexOf(dragEl) === -1) {
self.alignables.push(dragEl);
}
});
self.autoAlign();
}
// Set element opacity when element has been appended
setTimeout(function () {
C.setOpacity(self.$dropZone.children('.h5p-label'), 'background', self.backgroundOpacity);
C.setOpacity(self.$dropZone.children('.h5p-inner'), 'background', self.backgroundOpacity);
}, 0);
};
/**
* Find index of given alignable
*
* @param {jQuery} $alignable
* @return {number}
*/
DropZone.prototype.getIndexOf = function ($alignable) {
var self = this;
for (var i = 0; i < self.alignables.length; i++) {
if (self.alignables[i][0] === $alignable[0]) {
return i;
}
}
return -1
};
/**
* Remove alignable
*
* @param {jQuery} $alignable
*/
DropZone.prototype.removeAlignable = function ($alignable) {
var self = this;
// Find alignable index
var index = self.getIndexOf($alignable);
if (index !== -1)Â {
// Remove alignable
self.alignables.splice(index, 1);
if (self.autoAlignTimer === undefined) {
// Schedule re-aligment of alignables left
self.autoAlignTimer = setTimeout(function () {
delete self.autoAlignTimer;
self.autoAlign();
}, 1);
}
}
}
/**
* Auto-align alignable elements inside drop zone.
*/
DropZone.prototype.autoAlign = function () {
var self = this;
// Determine container size in order to calculate percetages
var containerSize = self.$dropZone.parent()[0].getBoundingClientRect();
// Calcuate borders and spacing values in percetage
var spacing = {
x: (self.autoAlignEnabled.spacing / self.autoAlignEnabled.size.width) * 100,
y: (self.autoAlignEnabled.spacing / self.autoAlignEnabled.size.height) * 100
};
// Determine coordinates for first 'spot'
var pos = {
x: self.x + spacing.x,
y: self.y + spacing.y
};
// Determine space inside drop zone
var dropZoneSize = self.$dropZone[0].getBoundingClientRect();
var space = {
x: dropZoneSize.width - (spacing.x * 2),
y: dropZoneSize.height - (spacing.y * 2)
};
// Set current space left inside drop zone
var spaceLeft = {
x: space.x,
y: space.y
};
// Set height for the active row of elements
var currentRowHeight = 0;
// Current alignable element and it's size
var $alignable, alignableSize;
/**
* Helper doing the actual positioning of the element + recalculating
* next position and space left.
*
* @private
*/
var alignElement = function () {
// Position element at current spot
$alignable.css({
left: pos.x + '%',
top: pos.y + '%'
});
// Update horizontal space left + next position
var spaceDiffX = (alignableSize.width + self.autoAlignEnabled.spacing);
spaceLeft.x -= spaceDiffX;
pos.x += (spaceDiffX / containerSize.width) * 100;
// Keep track of the highest element in this row
var spaceDiffY = (alignableSize.height + self.autoAlignEnabled.spacing);
if (spaceDiffY > currentRowHeight) {
currentRowHeight = spaceDiffY;
}
};
// Try to order and align the alignables inside the drop zone
// (in the order they were added)
for (var i = 0; i < self.alignables.length; i++) {
// Determine alignable size
$alignable = self.alignables[i];
alignableSize = $alignable[0].getBoundingClientRect();
// Try to fit on the current row
if (spaceLeft.x >= alignableSize.width) {
alignElement();
}
else {
// Did not fit, try next row
// Reset X values
spaceLeft.x = space.x;
pos.x = self.x + spacing.x;
// Bump Y values
if (currentRowHeight) {
// Update Y space and position according to previous row height
spaceLeft.y -= currentRowHeight;
pos.y += (currentRowHeight / containerSize.height) * 100;
// Reset
currentRowHeight = 0;
}
if (spaceLeft.y <= 0) {
return; // No more vertical space left, stop all aliging
}
alignElement();
}
}
};
return C;
})(H5P.jQuery);
;
var H5P = H5P || {};
/**
* Constructor.
*
* @param {object} params Options for this library.
* @param {string} contentPath The path to our content folder.
*/
H5P.Text = function (params, id) {
this.text = params.text === undefined ? '
New text' : params.text;
};
/**
* Wipe out the content of the wrapper and put our HTML in it.
*
* @param {jQuery} $wrapper
*/
H5P.Text.prototype.attach = function ($wrapper) {
$wrapper.addClass('h5p-text').html(this.text);
};;
var H5P = H5P || {};
H5P.Summary = H5P.Summary || {};
H5P.Summary.StopWatch = (function () {
/**
* @class {H5P.Summary.StopWatch}
* @constructor
*/
function StopWatch() {
/**
* @property {number} duration in ms
*/
this.duration = 0;
}
/**
* Starts the stop watch
*
* @public
* @return {H5P.Summary.StopWatch}
*/
StopWatch.prototype.start = function(){
/**
* @property {number}
*/
this.startTime = Date.now();
return this;
};
/**
* Stops the stopwatch, and returns the duration in seconds.
*
* @public
* @return {number}
*/
StopWatch.prototype.stop = function(){
this.duration = this.duration + Date.now() - this.startTime;
return this.passedTime();
};
/**
* Sets the duration to 0
*
* @public
*/
StopWatch.prototype.reset = function(){
this.duration = 0
};
/**
* Returns the passed time in seconds
*
* @public
* @return {number}
*/
StopWatch.prototype.passedTime = function(){
return Math.round(this.duration / 10) / 100;
};
return StopWatch;
})();;
var H5P = H5P || {};
H5P.Summary = H5P.Summary || {};
H5P.Summary.XApiEventBuilder = (function ($, EventDispatcher) {
/**
* @typedef {object} LocalizedString
* @property {string} en-US
*/
/**
* @class {H5P.Summary.XApiEventDefinitionBuilder}
* @constructor
*/
function XApiEventDefinitionBuilder(){
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.name
* @property {string} attributes.description
* @property {string} attributes.interactionType
* @property {string} attributes.correctResponsesPattern
* @property {object} attributes.optional
*/
this.attributes = {};
}
XApiEventDefinitionBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventDefinitionBuilder.prototype.constructor = XApiEventDefinitionBuilder;
/**
* Sets name
* @param {string} name
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.name = function (name) {
this.attributes.name = name;
return this;
};
/**
* Question text and any additional information to generate the report.
* @param {string} description
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.description = function (description) {
this.attributes.description = description;
return this;
};
/**
* Type of the interaction.
* @param {string} interactionType
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#interaction-types|xAPI Spec}
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.interactionType = function (interactionType) {
this.attributes.interactionType = interactionType;
return this;
};
/**
* A pattern for determining the correct answers of the interaction
* @param {string[]} correctResponsesPattern
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#response-patterns|xAPI Spec}
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.correctResponsesPattern = function (correctResponsesPattern) {
this.attributes.correctResponsesPattern = correctResponsesPattern;
return this;
};
/**
* Sets optional attributes
* @param {object} optional Can have one of the following configuration objects: choices, scale, source, target, steps
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.optional = function (optional) {
this.attributes.optional = optional;
return this;
};
/**
* @return {object}
*/
XApiEventDefinitionBuilder.prototype.build = function () {
var definition = {};
// sets attributes
setAttribute(definition, 'name', localizeToEnUS(this.attributes.name));
setAttribute(definition, 'description', localizeToEnUS(this.attributes.description));
setAttribute(definition, 'interactionType', this.attributes.interactionType);
setAttribute(definition, 'correctResponsesPattern', this.attributes.correctResponsesPattern);
setAttribute(definition, 'type', 'http://adlnet.gov/expapi/activities/cmi.interaction');
// adds the optional object to the definition
if(this.attributes.optional){
$.extend(definition, this.attributes.optional);
}
return definition;
};
// -----------------------------------------------------
/**
*
* @constructor
*/
function XApiEventResultBuilder(){
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.completion
* @property {boolean} attributes.success
* @property {boolean} attributes.response
* @property {number} attributes.rawScore
* @property {number} attributes.maxScore
*/
this.attributes = {};
}
XApiEventResultBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventResultBuilder.prototype.constructor = XApiEventResultBuilder;
/**
* @param {boolean} completion
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.completion = function (completion) {
this.attributes.completion = completion;
return this;
};
/**
* @param {boolean} success
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.success = function (success) {
this.attributes.success = success;
return this;
};
/**
* @param {number} duration The duraction in seconds
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.duration = function (duration) {
this.attributes.duration = duration;
return this;
};
/**
* Sets response
* @param {string|string[]} response
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.response = function (response) {
this.attributes.response = (typeof response === 'string') ? response : response.join('[,]');
return this;
};
/**
* Sets the score, and max score
* @param {number} score
* @param {number} maxScore
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.score = function (score, maxScore) {
this.attributes.rawScore = score;
this.attributes.maxScore = maxScore;
return this;
};
/**
* Builds the result object
* @return {object}
*/
XApiEventResultBuilder.prototype.build = function () {
var result = {};
setAttribute(result, 'response', this.attributes.response);
setAttribute(result, 'completion', this.attributes.completion);
setAttribute(result, 'success', this.attributes.success);
if(isDefined(this.attributes.duration)){
setAttribute(result, 'duration','PT' + this.attributes.duration + 'S');
}
// sets score
if (isDefined(this.attributes.rawScore)) {
result.score = {};
setAttribute(result.score, 'raw', this.attributes.rawScore);
if (isDefined(this.attributes.maxScore) && this.attributes.maxScore > 0) {
setAttribute(result.score, 'min', 0);
setAttribute(result.score, 'max', this.attributes.maxScore);
setAttribute(result.score, 'min', 0);
setAttribute(result.score, 'scaled', Math.round(this.attributes.rawScore / this.attributes.maxScore * 10000) / 10000);
}
}
return result;
};
// -----------------------------------------------------
/**
* @class {H5P.Summary.XApiEventBuilder}
*/
function XApiEventBuilder() {
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.contentId
* @property {string} attributes.subContentId
*/
this.attributes = {};
}
XApiEventBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventBuilder.prototype.constructor = XApiEventBuilder;
/**
* @param {object} verb
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.verb = function (verb) {
this.attributes.verb = verb;
return this;
};
/**
* @param {string} name
* @param {string} mbox
* @param {string} objectType
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.actor = function (name, mbox, objectType) {
this.attributes.actor = {
name: name,
mbox: mbox,
objectType: objectType
};
return this;
};
/**
* Sets contentId
* @param {string} contentId
* @param {string} [subContentId]
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.contentId = function (contentId, subContentId) {
this.attributes.contentId = contentId;
this.attributes.subContentId = subContentId;
return this;
};
/**
* Sets parent in context
* @param {string} parentContentId
* @param {string} [parentSubContentId]
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.context = function (parentContentId, parentSubContentId) {
this.attributes.parentContentId = parentContentId;
this.attributes.parentSubContentId = parentSubContentId;
return this;
};
/**
* @param {object} result
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.result = function (result) {
this.attributes.result = result;
return this;
};
/**
* @param {object} objectDefinition
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.objectDefinition = function (objectDefinition) {
this.attributes.objectDefinition = objectDefinition;
return this;
};
/**
* Returns the buildt event
* @public
* @return {H5P.XAPIEvent}
*/
XApiEventBuilder.prototype.build = function(){
var event = new H5P.XAPIEvent();
event.setActor();
event.setVerb(this.attributes.verb);
// sets context
if(this.attributes.parentContentId || this.attributes.parentSubContentId){
event.data.statement.context = {
'contextActivities': {
'parent': [
{
'id': getContentXAPIId(this.attributes.parentContentId, this.attributes.parentSubContentId),
'objectType': "Activity"
}
]
}
};
}
event.data.statement.object = {
'id': getContentXAPIId(this.attributes.contentId, this.attributes.subContentId),
'objectType': 'Activity'
};
setAttribute(event.data, 'actor', this.attributes.actor);
setAttribute(event.data.statement, 'result', this.attributes.result);
setAttribute(event.data.statement.object, 'definition', this.attributes.objectDefinition);
// sets h5p specific attributes
if(event.data.statement.object.definition && (this.attributes.contentId || this.attributes.subContentId)) {
var extensions = event.data.statement.object.definition.extensions = {};
setAttribute(extensions, 'http://h5p.org/x-api/h5p-local-content-id', this.attributes.contentId);
setAttribute(extensions, 'http://h5p.org/x-api/h5p-subContentId', this.attributes.subContentId);
}
return event;
};
/**
* Creates a Localized String object for en-US
*
* @param str
* @return {LocalizedString}
*/
var localizeToEnUS = function(str){
if(str != undefined){
return {
'en-US': cleanString(str)
};
}
};
/**
* Generates an id for the content
* @param {string} contentId
* @param {string} [subContentId]
*
* @see {@link https://github.com/h5p/h5p-php-library/blob/master/js/h5p-x-api-event.js#L240-L249}
* @return {string}
*/
var getContentXAPIId = function (contentId, subContentId) {
if (contentId && H5PIntegration && H5PIntegration.contents) {
var id = H5PIntegration.contents['cid-' + contentId].url;
if (subContentId) {
id += '?subContentId=' + subContentId;
}
return id;
}
};
/**
* Removes html elements from string
*
* @param {string} str
* @return {string}
*/
var cleanString = function (str) {
return $('
' + str + '
').text().trim();
};
var isDefined = function(val){
return typeof val !== 'undefined';
};
function setAttribute(obj, key, value, required){
if(isDefined(value)){
obj[key] = value;
} else if (required) {
console.error("xApiEventBuilder: No value for [" + key + "] in", obj);
}
}
/**
* Creates a new XApiEventBuilder
*
* @public
* @static
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.create = function(){
return new XApiEventBuilder();
};
/**
* Creates a new XApiEventDefinitionBuilder
*
* @public
* @static
* @return {XApiEventDefinitionBuilder}
*/
XApiEventBuilder.createDefinition = function(){
return new XApiEventDefinitionBuilder();
};
/**
* Creates a new XApiEventDefinitionBuilder
*
* @public
* @static
* @return {XApiEventResultBuilder}
*/
XApiEventBuilder.createResult = function(){
return new XApiEventResultBuilder();
};
/**
* Returns choice to be used with 'cmi.interaction' for Activity of type 'choice'
*
* @param {string} id
* @param {string} description
*
* @public
* @static
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#choice|xAPI-Spec}
* @return {object}
*/
XApiEventBuilder.createChoice = function(id, description){
return {
id: id,
description: localizeToEnUS(description)
};
};
/**
* Takes an array of correct ids, and joins them to a 'correct response pattern'
*
* @param {string[]} ids
*
* @public
* @static
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#choice|xAPI-Spec}
* @return {string}
*/
XApiEventBuilder.createCorrectResponsePattern = function(ids){
return ids.join('[,]');
};
/**
* Interaction types
*
* @readonly
* @enum {String}
*/
XApiEventBuilder.interactionTypes = {
CHOICE: 'choice',
COMPOUND: 'compound',
FILL_IN: 'fill-in',
MATCHING: 'matching',
TRUE_FALSE: 'true-false'
};
/**
* Verbs
*
* @readonly
* @enum {String}
*/
XApiEventBuilder.verbs = {
ANSWERED: 'answered'
};
return XApiEventBuilder;
})(H5P.jQuery, H5P.EventDispatcher);
;
H5P.Summary = (function ($, Question, XApiEventBuilder, StopWatch) {
function Summary(options, contentId, contentData) {
if (!(this instanceof H5P.Summary)) {
return new H5P.Summary(options, contentId);
}
this.id = this.contentId = contentId;
Question.call(this, 'summary');
this.offset = 0;
this.score = 0;
this.progress = 0;
this.answers = [];
this.answer = [];
this.errorCounts = [];
/**
* The key is panel index, returns an array of the answer indexes the user tried.
*
* @property {number[][]}
*/
this.userResponses = [];
/**
* The first key is panel index, and the second key is data-bit, value is index in panel
*
* @property {number[][]}
*/
this.dataBitMap = [];
// Remove empty summary to avoid JS-errors
if (options.summaries) {
options.summaries = options.summaries.filter(function (element) {
return element.summary !== undefined;
});
}
if (contentData && contentData.previousState !== undefined &&
contentData.previousState.progress !== undefined &&
contentData.previousState.answers) {
this.progress = contentData.previousState.progress || this.progress;
this.answers = contentData.previousState.answers || this.answers;
var currentProgress = this.progress;
// Do not count score screen as an error
if (this.progress >= options.summaries.length) {
currentProgress = options.summaries.length - 1;
}
for (var i = 0; i <= currentProgress; i++) {
if (this.errorCounts[i] === undefined) {
this.errorCounts[i] = 0;
}
if (this.answers[i]) {
this.score += this.answers[i].length;
this.errorCounts[i]++;
}
}
}
var that = this;
/**
* @property {StopWatch[]} Stop watches for tracking duration of slides
*/
this.stopWatches = [];
this.startStopWatch(this.progress);
this.options = H5P.jQuery.extend({}, {
response: {
scorePerfect:
{
title: "PERFECT!",
message: "You got everything correct on your first try. Be proud!"
},
scoreOver70:
{
title: "Great!",
message: "You got most of the statements correct on your first try!"
},
scoreOver40:
{
title: "Ok",
message: "You got some of the statements correct on your first try. There is still room for improvement."
},
scoreOver0:
{
title: "Not good",
message: "You need to work more on this"
}
},
summary: "You got @score of @total statements (@percent %) correct on your first try.",
resultLabel: "Your result:",
intro: "Choose the correct statement.",
solvedLabel: "Solved:",
scoreLabel: "Wrong answers:",
postUserStatistics: (H5P.postUserStatistics === true)
}, options);
this.summaries = that.options.summaries;
// Required questiontype contract function
this.showSolutions = function() {
// intentionally left blank, no solution view exists
};
// Required questiontype contract function
this.getMaxScore = function() {
return this.summaries.length;
};
this.getScore = function() {
var self = this;
// count single correct answers
return self.summaries.reduce(function(result, panel, index){
var userResponse = self.userResponses[index] || [];
return result + (self.correctOnFirstTry(userResponse) ? 1 : 0);
}, 0);
};
this.getTitle = function() {
return H5P.createTitle(this.options.intro);
};
this.getCurrentState = function () {
return {
progress: this.progress,
answers: this.answers
};
};
}
Summary.prototype = Object.create(Question.prototype);
Summary.prototype.constructor = Summary;
/**
* Registers DOM elements before they are attached.
* Called from H5P.Question.
*/
Summary.prototype.registerDomElements = function () {
// Register task content area
this.setContent(this.createQuestion());
};
// Function for attaching the multichoice to a DOM element.
Summary.prototype.createQuestion = function() {
var that = this;
var id = 0; // element counter
var elements = [];
var $ = H5P.jQuery;
this.$myDom = $('
', {
'class': 'summary-content'
});
if (that.summaries === undefined || that.summaries.length === 0) {
return;
}
// Create array objects
for (var panelIndex = 0; panelIndex < that.summaries.length; panelIndex++) {
if (!(that.summaries[panelIndex].summary && that.summaries[panelIndex].summary.length)) {
continue;
}
elements[panelIndex] = {
tip: that.summaries[panelIndex].tip,
summaries: []
};
for (var summaryIndex = 0; summaryIndex < that.summaries[panelIndex].summary.length; summaryIndex++) {
var isAnswer = (summaryIndex === 0);
that.answer[id] = isAnswer; // First claim is correct
// create mapping from data-bit to index in panel
that.dataBitMap[panelIndex] = this.dataBitMap[panelIndex] || [];
that.dataBitMap[panelIndex][id] = summaryIndex;
// checks the answer and updates the user response array
if(that.answers[panelIndex] && (that.answers[panelIndex].indexOf(id) !== -1)){
this.storeUserResponse(panelIndex, summaryIndex);
}
// adds to elements
elements[panelIndex].summaries[summaryIndex] = {
id: id++,
text: that.summaries[panelIndex].summary[summaryIndex]
};
}
// if we have progressed passed this point, the success pattern must also be saved
if(panelIndex < that.progress){
this.storeUserResponse(panelIndex, 0);
}
// Randomize elements
for (var k = elements[panelIndex].summaries.length - 1; k > 0; k--) {
var j = Math.floor(Math.random() * (k + 1));
var temp = elements[panelIndex].summaries[k];
elements[panelIndex].summaries[k] = elements[panelIndex].summaries[j];
elements[panelIndex].summaries[j] = temp;
}
}
// Create content panels
var $summary_container = $('
');
var $summary_list = $('
');
var $evaluation = $('
');
var $evaluation_content = $('
' + that.options.intro + '
');
var $score = $('
');
var $options = $('
');
var $progress = $('
');
var options_padding = parseInt($options.css('paddingLeft'));
if (this.score) {
$score.html(that.options.scoreLabel + ' ' + this.score).show();
}
// Insert content
$summary_container.append($summary_list);
this.$myDom.append($summary_container);
this.$myDom.append($evaluation);
this.$myDom.append($options);
$evaluation.append($evaluation_content);
$evaluation.append($evaluation);
$evaluation.append($progress);
$evaluation.append($score);
/**
* Handle selected alternative
*
* @param {jQuery} $el Selected element
* @param {boolean} [setFocus] Set focus on first element of next panel.
* Used when alt was selected with keyboard.
*/
var selectedAlt = function ($el, setFocus) {
var nodeId = Number($el.attr('data-bit'));
var panelId = Number($el.parent().data('panel'));
if (that.errorCounts[panelId] === undefined) {
that.errorCounts[panelId] = 0;
}
that.storeUserResponse(panelId, nodeId);
// Correct answer?
if (that.answer[nodeId]) {
that.stopStopWatch(panelId);
that.progress++;
var position = $el.position();
var summary = $summary_list.position();
var $answer = $('
' + $el.html() + '');
$progress.html(that.options.solvedLabel + ' ' + (panelId + 1) + '/' + that.summaries.length);
// Insert correct claim into summary list
$summary_list.append($answer);
$summary_container.addClass('has-results');
that.adjustTargetHeight($summary_container, $summary_list, $answer);
// Move into position over clicked element
$answer.css({display: 'block', width: $el.css('width'), height: $el.css('height')});
$answer.css({position: 'absolute', top: position.top, left: position.left});
$answer.css({backgroundColor: '#9dd8bb', border: ''});
setTimeout(function () {
$answer.css({backgroundColor: ''});
}, 1);
//$answer.animate({backgroundColor: '#eee'}, 'slow');
var panel = parseInt($el.parent().attr('data-panel'));
var $curr_panel = $('.h5p-panel:eq(' + panel + ')', that.$myDom);
var $next_panel = $('.h5p-panel:eq(' + (panel + 1) + ')', that.$myDom);
var finished = ($next_panel.length === 0);
var height = $curr_panel.parent().css('height');
// Disable panel while waiting for animation
$curr_panel.addClass('panel-disabled');
// Update tip:
$evaluation_content.find('.joubel-tip-container').remove();
if (elements[that.progress] !== undefined &&
elements[that.progress].tip !== undefined &&
elements[that.progress].tip.trim().length > 0) {
$evaluation_content.append(H5P.JoubelUI.createTip(elements[that.progress].tip));
}
$answer.animate(
{
top: summary.top + that.offset,
left: '-=' + options_padding + 'px',
width: '+=' + (options_padding * 2) + 'px'
},
{
complete: function() {
// Remove position (becomes inline);
$(this).css('position', '').css({
width: '',
height: '',
top: '',
left: ''
});
$summary_container.css('height', '');
// Calculate offset for next summary item
var tpadding = parseInt($answer.css('paddingTop')) * 2;
var tmargin = parseInt($answer.css('marginBottom'));
var theight = parseInt($answer.css('height'));
that.offset += theight + tpadding + tmargin + 1;
// Fade out current panel
$curr_panel.fadeOut('fast', function () {
$curr_panel.parent().css('height', 'auto');
// Show next panel if present
if (!finished) {
// start next timer
that.startStopWatch(that.progress);
$next_panel.fadeIn('fast');
// Focus first element of next panel
if (setFocus) {
$next_panel.children().get(0).focus();
}
} else {
// Hide intermediate evaluation
$evaluation_content.html(that.options.resultLabel);
that.doFinalEvaluation();
}
that.trigger('resize');
});
}
}
);
}
else {
// Remove event handler (prevent repeated clicks) and mouseover effect
$el.off('click');
$el.addClass('summary-failed');
$el.removeClass('summary-claim-unclicked');
$evaluation.children('.summary-score').css('display', 'block');
$score.html(that.options.scoreLabel + ' ' + (++that.score));
that.errorCounts[panelId]++;
if (that.answers[panelId] === undefined) {
that.answers[panelId] = [];
}
that.answers[panelId].push(nodeId);
}
that.trigger('resize');
$el.attr('tabindex', '-1');
that.triggerXAPI('interacted');
// Trigger answered xAPI event on first try for the current
// statement group
if (that.userResponses[panelId].length === 1) {
that.trigger(that.createXApiAnsweredEvent(
that.summaries[panelId],
that.userResponses[panelId] || [],
panelId,
that.timePassedInStopWatch(panelId)));
}
// Trigger overall answered xAPI event when finished
if (finished) {
that.triggerXAPIScored(that.getScore(), that.getMaxScore(), 'answered');
}
};
$progress.html(that.options.solvedLabel + ' ' + this.progress + '/' + that.summaries.length);
// Add elements to content
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (i < that.progress) { // i is panelId
for (var j = 0; j < element.summaries.length; j++) {
var sum = element.summaries[j];
if (that.answer[sum.id]) {
$summary_list.append('
' + sum.text + '');
$summary_container.addClass('has-results');
break;
}
}
// Cannot use continue; due to id/animation system
}
var $page = $('
');
// Create initial tip for first summary-list if tip is available
if (i==0 && element.tip !== undefined && element.tip.trim().length > 0) {
$evaluation_content.append(H5P.JoubelUI.createTip(element.tip));
}
for (var j = 0; j < element.summaries.length; j++) {
var summaryLineClass = 'summary-claim-unclicked';
// If progress is at current task
if (that.progress === i && that.answers[that.progress]) {
// Check if there are any previous wrong answers.
for (var k = 0; k < that.answers[that.progress].length; k++) {
if (that.answers[that.progress][k] === element.summaries[j].id) {
summaryLineClass = 'summary-failed';
break;
}
}
}
var $node = $('' +
'
' +
element.summaries[j].text +
'');
// Do not add click event for failed nodes
if (summaryLineClass === 'summary-failed') {
$page.append($node);
continue;
}
$node.click(function() {
selectedAlt($(this));
}).keypress(function (e) {
var keyPressed = e.which;
// 32 - space
if (keyPressed === 32) {
selectedAlt($(this), true);
e.preventDefault();
}
});
$page.append($node);
}
$options.append($page);
}
if (that.progress === elements.length) {
$evaluation_content.html(that.options.resultLabel);
that.doFinalEvaluation();
}
else {
// Show first panel
$('.h5p-panel:eq(' + (that.progress) + ')', that.$myDom).css({display: 'block'});
if (that.progress) {
that.offset = ($('.summary-claim-unclicked:visible:first', that.$myDom).outerHeight() * that.errorCounts.length);
}
}
that.trigger('resize');
return this.$myDom;
};
/**
* Calculate final score and display feedback.
*
* @param container
* @param options_panel
* @param list
* @param score
*/
Summary.prototype.doFinalEvaluation = function () {
var that = this;
var error_count = this.countErrors();
// Calculate percentage
var percent = 100 - (error_count / that.errorCounts.length * 100);
// Find evaluation message
var from = 0;
for (var i in that.options.response) {
switch (i) {
case "scorePerfect":
from = 100;
break;
case "scoreOver70":
from = 70;
break;
case "scoreOver40":
from = 40;
break;
case "scoreOver0":
from = 0;
break;
}
if (percent >= from) {
break;
}
}
// Show final evaluation
var summary = that.options.summary.replace('@score', that.summaries.length - error_count).replace('@total', that.summaries.length).replace('@percent', Math.round(percent));
this.setFeedback(summary, that.summaries.length - error_count, that.summaries.length);
that.trigger('resize');
};
/**
* Resets the complete task back to its' initial state.
* Used for contracts.
*/
Summary.prototype.resetTask = function () {
// Summary is not yet able to Reset itself
};
/**
* Adjust height of container.
*
* @param container
* @param elements
* @param el
*/
Summary.prototype.adjustTargetHeight = function (container, elements, el) {
var new_height = parseInt(elements.outerHeight()) + parseInt(el.outerHeight()) + parseInt(el.css('marginBottom')) + parseInt(el.css('marginTop'));
if (new_height > parseInt(container.css('height'))) {
container.animate({height: new_height});
}
};
/**
* Count amount of wrong answers
*
* @returns {number}
*/
Summary.prototype.countErrors = function() {
var error_count = 0;
// Count boards without errors
for (var i = 0; i < this.summaries.length; i++) {
if (this.errorCounts[i] === undefined) {
error_count++;
}
else {
error_count += this.errorCounts[i] ? 1 : 0;
}
}
return error_count;
};
/**
* Returns the choices array for xApi statements
*
* @param {String[]} answers
*
* @return {{ choices: []}}
*/
Summary.prototype.getXApiChoices = function (answers) {
var choices = answers.map(function(answer, index){
return XApiEventBuilder.createChoice(index.toString(), answer);
});
return {
choices: choices
}
};
/**
* Saves the user response
*
* @param {number} questionIndex
* @param {number} answerIndex
*/
Summary.prototype.storeUserResponse = function (questionIndex, answerIndex) {
var self = this;
if(self.userResponses[questionIndex] === undefined){
self.userResponses[questionIndex] = [];
}
self.userResponses[questionIndex].push(this.dataBitMap[questionIndex][answerIndex]);
};
/**
* Starts a stopwatch for indexed slide
*
* @param {number} index
*/
Summary.prototype.startStopWatch = function (index) {
this.stopWatches[index] = this.stopWatches[index] || new StopWatch();
this.stopWatches[index].start();
};
/**
* Stops a stopwatch for indexed slide
*
* @param {number} [index]
*/
Summary.prototype.stopStopWatch = function (index) {
if(this.stopWatches[index]){
this.stopWatches[index].stop();
}
};
/**
* Returns the passed time in seconds of a stopwatch on an indexed slide,
* or 0 if not existing
*
* @param {number} index
* @return {number}
*/
Summary.prototype.timePassedInStopWatch = function (index) {
if(this.stopWatches[index] !== undefined){
return this.stopWatches[index].passedTime();
}
else {
// if not created, return no passed time,
return 0;
}
};
/**
* Returns the time the user has spent on all questions so far
*
* @return {number}
*/
Summary.prototype.getTotalPassedTime = function () {
return this.stopWatches
.filter(function(watch){
return watch != undefined;
})
.reduce(function(sum, watch){
return sum + watch.passedTime();
}, 0);
};
/**
* Creates an xAPI answered event for a single statement list
*
* @param {object} panel
* @param {number[]} userAnswer
* @param {number} panelIndex
* @param {number} duration
*
* @return {H5P.XAPIEvent}
*/
Summary.prototype.createXApiAnsweredEvent = function (panel, userAnswer, panelIndex, duration) {
var self = this;
// creates the definition object
var definition = XApiEventBuilder.createDefinition()
.name('Summary statement')
.description(self.options.intro)
.interactionType(XApiEventBuilder.interactionTypes.CHOICE)
.correctResponsesPattern(['0'])
.optional(self.getXApiChoices(panel.summary))
.build();
// create the result object
var result = XApiEventBuilder.createResult()
.response(userAnswer.join('[,]'))
.duration(duration)
.score((self.correctOnFirstTry(userAnswer) ? 1 : 0), 1)
.build();
return XApiEventBuilder.create()
.verb(XApiEventBuilder.verbs.ANSWERED)
.objectDefinition(definition)
.context(self.contentId, self.subContentId)
.contentId(self.contentId, panel.subContentId)
.result(result)
.build();
};
Summary.prototype.correctOnFirstTry = function(userAnswer){
return (userAnswer.length === 1) && userAnswer[0] === 0;
};
/**
* Retrieves the xAPI data necessary for generating result reports.
*
* @return {object}
*/
Summary.prototype.getXAPIData = function(){
var self = this;
// create array with userAnswer
var children = self.summaries.map(function(panel, index) {
var userResponse = self.userResponses[index] || [];
var duration = self.timePassedInStopWatch(index);
var event = self.createXApiAnsweredEvent(panel, userResponse, index, duration);
return {
statement: event.data.statement
}
});
var result = XApiEventBuilder.createResult()
.score(self.getScore(), self.getMaxScore())
.duration(self.getTotalPassedTime())
.build();
// creates the definition object
var definition = XApiEventBuilder.createDefinition()
.interactionType(XApiEventBuilder.interactionTypes.COMPOUND)
.name(self.getTitle())
.description(self.options.intro)
.build();
var xAPIEvent = XApiEventBuilder.create()
.verb(XApiEventBuilder.verbs.ANSWERED)
.contentId(self.contentId, self.subContentId)
.context(self.getParentAttribute('contentId'), self.getParentAttribute('subContentId'))
.objectDefinition(definition)
.result(result)
.build();
return {
statement: xAPIEvent.data.statement,
children: children
};
};
/**
* Returns an attribute from this.parent if it exists
*
* @param {string} attributeName
* @return {*|undefined}
*/
Summary.prototype.getParentAttribute = function (attributeName) {
var self = this;
if(self.parent !== undefined){
return self.parent[attributeName];
}
};
return Summary;
})(H5P.jQuery, H5P.Question, H5P.Summary.XApiEventBuilder, H5P.Summary.StopWatch);
;
var H5P = H5P || {};
/**
* A class that easily helps your create awesome drag and drop.
*
* @param {H5P.DragNBar} DnB
* @param {jQuery} $container
* @returns {undefined}
*/
H5P.DragNDrop = function (dnb, $container) {
H5P.EventDispatcher.call(this);
this.dnb = dnb;
this.$container = $container;
this.scrollLeft = 0;
this.scrollTop = 0;
};
// Inherit support for events
H5P.DragNDrop.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.DragNDrop.prototype.constructor = H5P.DragNDrop;
/**
* Set the current element
*
* @method setElement
* @param {j@uery} $element
*/
H5P.DragNDrop.prototype.setElement = function ($element) {
this.$element = $element;
};
/**
* Start tracking the mouse.
*
* @param {jQuery} $element
* @param {Number} x Start X coordinate
* @param {Number} y Start Y coordinate
* @returns {undefined}
*/
H5P.DragNDrop.prototype.press = function ($element, x, y) {
var that = this;
var eventData = {
instance: this
};
H5P.$window
.mousemove(eventData, H5P.DragNDrop.moveHandler)
.bind('mouseup', eventData, H5P.DragNDrop.release);
H5P.$body
// With user-select: none uncommented, after moving a drag and drop element, if I hover over something that changes transparancy on hover IE10 on WIN7 crashes
// TODO: Add user-select and -ms-user-select later if IE10 stops bugging
.css({'-moz-user-select': 'none', '-webkit-user-select': 'none'/*, 'user-select': 'none', '-ms-user-select': 'none'*/})
.attr('unselectable', 'on')[0]
.onselectstart = H5P.$body[0].ondragstart = function () {
return false;
};
that.containerOffset = $element.offsetParent().offset();
this.$element = $element;
this.moving = false;
this.startX = x;
this.startY = y;
this.marginX = parseInt($element.css('marginLeft')) + parseInt($element.css('marginRight'));
this.marginY = parseInt($element.css('marginTop')) + parseInt($element.css('marginBottom'));
var offset = $element.offset();
this.adjust = {
x: x - offset.left + this.marginX,
y: y - offset.top - this.marginY
};
if (that.dnb && that.dnb.newElement) {
this.move(x, y);
}
};
/**
* Handles mouse move events.
*
* @param {Event} event
*/
H5P.DragNDrop.moveHandler = function (event) {
event.stopPropagation();
event.data.instance.move(event.pageX, event.pageY);
};
/**
* Handles mouse movements.
*
* @param {number} x
* @param {number} y
*/
H5P.DragNDrop.prototype.move = function (x, y) {
var that = this;
if (!that.moving) {
if (that.startMovingCallback !== undefined && !that.startMovingCallback(x, y)) {
return;
}
// Start moving
that.moving = true;
that.$element.addClass('h5p-moving');
}
x -= that.adjust.x;
y -= that.adjust.y;
var posX = x - that.containerOffset.left + that.scrollLeft;
var posY = y - that.containerOffset.top + that.scrollTop;
if (that.snap !== undefined) {
posX = Math.round(posX / that.snap) * that.snap;
posY = Math.round(posY / that.snap) * that.snap;
}
// Do not move outside of minimum values.
if (that.min !== undefined) {
if (posX < that.min.x) {
posX = that.min.x;
x = that.min.x + that.containerOffset.left - that.scrollLeft;
}
if (posY < that.min.y) {
posY = that.min.y;
y = that.min.y + that.containerOffset.top - that.scrollTop;
}
}
if (that.dnb && that.dnb.newElement && posY >= 0) {
that.min.y = 0;
}
// Do not move outside of maximum values.
if (that.max !== undefined) {
if (posX > that.max.x) {
posX = that.max.x;
x = that.max.x + that.containerOffset.left - that.scrollLeft;
}
if (posY > that.max.y) {
posY = that.max.y;
y = that.max.y + that.containerOffset.top - that.scrollTop;
}
}
// Show transform panel if element has moved
var startX = that.startX - that.adjust.x - that.containerOffset.left + that.scrollLeft;
var startY = that.startY - that.adjust.y - that.containerOffset.top + that.scrollTop;
if (!that.snap && (posX !== startX || posY !== startY)) {
that.trigger('showTransformPanel');
}
else if (that.snap) {
var xChanged = (Math.round(posX / that.snap) * that.snap) !==
(Math.round(startX / that.snap) * that.snap);
var yChanged = (Math.round(posY / that.snap) * that.snap) !==
(Math.round(startY / that.snap) * that.snap);
if (xChanged || yChanged) {
that.trigger('showTransformPanel');
}
}
that.$element.css({left: posX, top: posY});
if (that.dnb) {
that.dnb.updateCoordinates();
}
if (that.moveCallback !== undefined) {
that.moveCallback(x, y, that.$element);
}
};
/**
* Stop tracking the mouse.
*
* @param {Object} event
* @returns {undefined}
*/
H5P.DragNDrop.release = function (event) {
var that = event.data.instance;
H5P.$window
.unbind('mousemove', H5P.DragNDrop.moveHandler)
.unbind('mouseup', H5P.DragNDrop.release);
H5P.$body
.css({'-moz-user-select': '', '-webkit-user-select': ''/*, 'user-select': '', '-ms-user-select': ''*/})
.removeAttr('unselectable')[0]
.onselectstart = H5P.$body[0].ondragstart = null;
if (that.releaseCallback !== undefined) {
that.releaseCallback();
}
if (that.moving) {
that.$element.removeClass('h5p-moving');
if (that.stopMovingCallback !== undefined) {
that.stopMovingCallback(event);
}
}
// trigger to hide the transform panel unless it was activated
// through the context menu
that.trigger('hideTransformPanel');
};
;
/*global H5P*/
H5P.DragNResize = (function ($, EventDispatcher) {
/**
* Constructor!
*
* @class H5P.DragNResize
* @param {H5P.jQuery} $container
*/
function C($container) {
var self = this;
this.$container = $container;
self.disabledModifiers = false;
EventDispatcher.call(this);
// Override settings for snapping to grid, and locking aspect ratio.
H5P.$body.keydown(function (event) {
if (self.disabledModifiers) {
return;
}
if (event.keyCode === 17) {
// Ctrl
self.revertSnap = true;
}
else if (event.keyCode === 16) {
// Shift
self.revertLock = true;
}
}).keyup(function (event) {
if (self.disabledModifiers) {
return;
}
if (event.keyCode === 17) {
// Ctrl
self.revertSnap = false;
}
else if (event.keyCode === 16) {
// Shift
self.revertLock = false;
}
});
}
// Inheritance
C.prototype = Object.create(EventDispatcher.prototype);
C.prototype.constructor = C;
/**
* Gives the given element a resize handle.
*
* @param {H5P.jQuery} $element
* @param {Object} [options]
* @param {boolean} [options.lock]
*/
C.prototype.add = function ($element, options) {
var that = this;
// Array with position of handles
var cornerPositions = ['nw', 'ne', 'sw', 'se'];
var edgePositions = ['n', 'w', 'e', 's'];
var addResizeHandle = function (position) {
$('
', {
'class': 'h5p-dragnresize-handle ' + position
}).mousedown(function (event) {
that.lock = (options && options.lock);
that.$element = $element;
that.press(event.clientX, event.clientY, position);
}).data('position', position)
.appendTo($element);
};
cornerPositions.forEach(function (pos) {
addResizeHandle(pos);
});
// Add edge handles
if (!options || !options.lock) {
edgePositions.forEach(function (pos) {
addResizeHandle(pos);
});
}
};
/**
* Get paddings for the element
*/
C.prototype.getElementPaddings = function () {
return {
horizontal: Number(this.$element.css('padding-left').replace("px", "")) + Number(this.$element.css('padding-right').replace("px", "")),
vertical: Number(this.$element.css('padding-top').replace("px", "")) + Number(this.$element.css('padding-bottom').replace("px", ""))
};
};
/**
* Get borders for the element
* @returns {{horizontal: number, vertical: number}}
*/
C.prototype.getElementBorders = function () {
return {
horizontal: Number(this.$element.css('border-left-width').replace('px', '')) + Number(this.$element.css('border-right-width').replace('px', '')),
vertical: Number(this.$element.css('border-top-width').replace('px', '')) + Number(this.$element.css('border-bottom-width').replace('px', ''))
}
};
C.prototype.setContainerEm = function (containerEm) {
this.containerEm = containerEm;
};
/**
* Start resizing
*
* @param {number} x
* @param {number} y
* @param {String} [direction] Direction of resize
*/
C.prototype.press = function (x, y, direction) {
this.active = true;
var eventData = {
instance: this,
direction: direction
};
H5P.$window
.bind('mouseup', eventData, C.release)
.mousemove(eventData, C.move);
H5P.$body
.css({
'-moz-user-select': 'none',
'-webkit-user-select': 'none',
'user-select': 'none',
'-ms-user-select': 'none'
})
.attr('unselectable', 'on')[0]
.onselectstart = H5P.$body[0].ondragstart = function () {
return false;
};
this.startX = x;
this.startY = y;
this.padding = this.getElementPaddings();
this.borders = this.getElementBorders();
this.startWidth = this.$element.outerWidth();
this.startHeight = this.$element.outerHeight();
this.ratio = (this.startWidth / this.startHeight);
var position = this.$element.position();
this.left = position.left;
this.top = position.top;
this.containerWidth = this.$container.width();
this.containerHeight = this.$container.height();
// Set default values
this.newLeft = this.left;
this.newTop = this.top;
this.newWidth = this.startWidth;
this.newHeight = this.startHeight;
this.trigger('startResizing', eventData);
// Show transform panel
this.trigger('showTransformPanel');
};
/**
* Resize events
*
* @param {Event} event
*/
C.move = function (event) {
var direction = (event.data.direction ? event.data.direction : 'se');
var that = event.data.instance;
var moveW = (direction === 'nw' || direction === 'sw' || direction === 'w');
var moveN = (direction === 'nw' || direction === 'ne' || direction === 'n');
var movesHorizontal = (direction === 'w' || direction === 'e');
var movesVertical = (direction === 'n' || direction === 's');
var deltaX = that.startX - event.clientX;
var deltaY = that.startY - event.clientY;
that.minLeft = that.left + that.startWidth - H5P.DragNResize.MIN_SIZE;
that.minTop = that.top + that.startHeight - H5P.DragNResize.MIN_SIZE;
// Moving west
if (moveW) {
that.newLeft = that.left - deltaX;
that.newWidth = that.startWidth + deltaX;
// Check edge cases
if (that.newLeft < 0) {
that.newLeft = 0;
that.newWidth = that.left + that.startWidth;
}
else if (that.newLeft > that.minLeft) {
that.newLeft = that.minLeft;
that.newWidth = that.left - that.minLeft + that.startWidth;
}
// Snap west side
if (that.snap && !that.revertSnap) {
that.newLeft = Math.round(that.newLeft / that.snap) * that.snap;
// Make sure element does not snap east
if (that.newLeft > that.minLeft) {
that.newLeft = Math.floor(that.minLeft / that.snap) * that.snap;
}
that.newWidth = (that.left - that.newLeft) + that.startWidth;
}
}
else if (!movesVertical) {
that.newWidth = that.startWidth - deltaX;
// Snap width
if (that.snap && !that.revertSnap) {
that.newWidth = Math.round(that.newWidth / that.snap) * that.snap;
}
if (that.left + that.newWidth > that.containerWidth) {
that.newWidth = that.containerWidth - that.left;
}
}
// Moving north
if (moveN) {
that.newTop = that.top - deltaY;
that.newHeight = that.startHeight + deltaY;
// Check edge cases
if (that.newTop < 0) {
that.newTop = 0;
that.newHeight = that.top + that.startHeight;
}
else if (that.newTop > that.minTop) {
that.newTop = that.minTop;
that.newHeight = that.top - that.minTop + that.startHeight;
}
// Snap north
if (that.snap && !that.revertSnap) {
that.newTop = Math.round(that.newTop / that.snap) * that.snap;
// Make sure element does not snap south
if (that.newTop > that.minTop) {
that.newTop = Math.floor(that.minTop / that.snap) * that.snap;
}
that.newHeight = (that.top - that.newTop) + that.startHeight;
}
}
else if (!movesHorizontal) {
that.newHeight = that.startHeight - deltaY;
// Snap height
if (that.snap && !that.revertSnap) {
that.newHeight = Math.round(that.newHeight / that.snap) * that.snap;
}
if (that.top + that.newHeight > that.containerHeight) {
that.newHeight = that.containerHeight - that.top;
}
}
// Set min size
if (that.newWidth <= H5P.DragNResize.MIN_SIZE) {
that.newWidth = H5P.DragNResize.MIN_SIZE;
}
if (that.newHeight <= H5P.DragNResize.MIN_SIZE) {
that.newHeight = H5P.DragNResize.MIN_SIZE;
}
// Apply ratio lock
var lock = (that.revertLock ? !that.lock : that.lock);
if (lock) {
that.lockDimensions(moveW, moveN, movesVertical, movesHorizontal);
}
// Reduce size by padding and borders
that.finalWidth = that.newWidth;
that.finalHeight = that.newHeight;
if (that.$element.css('boxSizing') !== 'border-box') {
that.finalWidth -= (that.padding.horizontal + that.borders.horizontal);
that.finalHeight -= (that.padding.vertical + that.borders.vertical);
}
that.$element.css({
width: (that.finalWidth / that.containerEm) + 'em',
height: (that.finalHeight / that.containerEm) + 'em',
left: ((that.newLeft / that.containerWidth) * 100) + '%',
top: ((that.newTop / that.containerHeight) * 100) + '%'
});
that.trigger('moveResizing');
};
/**
* Changes element values depending on moving direction of the element
* @param isMovingWest
* @param isMovingNorth
* @param movesVertical
* @param movesHorizontal
*/
C.prototype.lockDimensions = function (isMovingWest, isMovingNorth, movesVertical, movesHorizontal) {
var self = this;
// Cap movement at top
var lockTop = function (isMovingNorth) {
if (!isMovingNorth) {
return;
}
self.newTop = self.top - (self.newHeight - self.startHeight);
// Edge case
if (self.newTop <= 0) {
self.newTop = 0;
}
};
// Expand to longest edge
if (movesVertical) {
this.newWidth = this.newHeight * this.ratio;
// Make sure locked ratio does not cause size to go below min size
if (this.newWidth < H5P.DragNResize.MIN_SIZE) {
this.newWidth = H5P.DragNResize.MIN_SIZE;
this.newHeight = H5P.DragNResize.MIN_SIZE / this.ratio;
}
}
else if (movesHorizontal) {
this.newHeight = this.newWidth / this.ratio;
// Make sure locked ratio does not cause size to go below min size
if (this.newHeight < H5P.DragNResize.MIN_SIZE) {
this.newHeight = H5P.DragNResize.MIN_SIZE;
this.newWidth = H5P.DragNResize.MIN_SIZE * this.ratio;
}
}
else if (this.newWidth / this.startWidth > this.newHeight / this.startHeight) {
// Expand to width
this.newHeight = this.newWidth / this.ratio;
}
else {
// Expand to height
this.newWidth = this.newHeight * this.ratio;
}
// Change top to match new height
if (isMovingNorth) {
lockTop(isMovingNorth);
if (self.newTop <= 0) {
self.newHeight = self.top + self.startHeight;
self.newWidth = self.newHeight * self.ratio;
}
}
else {
// Too high
if (this.top + this.newHeight > this.containerHeight) {
this.newHeight = this.containerHeight - this.top;
this.newWidth = this.newHeight * this.ratio;
}
}
// Change left to match new width
if (isMovingWest) {
this.newLeft = this.left - (this.newWidth - this.startWidth);
// Edge case
if (this.newLeft <= 0) {
this.newLeft = 0;
this.newWidth = this.left + this.startWidth;
this.newHeight = this.newWidth / this.ratio;
}
}
else {
// Too wide
if (this.left + this.newWidth > this.containerWidth) {
this.newWidth = this.containerWidth - this.left;
this.newHeight = this.newWidth / this.ratio;
}
}
// Need to re-lock top in case height changed
lockTop(isMovingNorth);
};
/**
* Stop resizing
*
* @param {Event} event
*/
C.release = function (event) {
var that = event.data.instance;
that.active = false;
H5P.$window
.unbind('mouseup', C.release)
.unbind('mousemove', C.move);
H5P.$body
.css({
'-moz-user-select': '',
'-webkit-user-select': '',
'user-select': '',
'-ms-user-select': ''
})
.removeAttr('unselectable')[0]
.onselectstart = H5P.$body[0].ondragstart = null;
if (that.newWidth !== that.startWidth ||
that.newHeight !== that.startHeight) {
// Stopped resizing send width and height in Ems
that.trigger('stoppedResizing', {
left: that.newLeft,
top: that.newTop,
width: that.finalWidth / that.containerEm,
height: that.finalHeight / that.containerEm
});
}
// Refocus element after resizing it. Apply timeout since focus is lost at the end of mouse event.
setTimeout(function () {
that.$element.focus();
}, 0);
// trigger to hide the transform panel unless it was activated
// through the context menu
that.trigger('hideTransformPanel');
};
/**
* Toggle modifiers when we are not interacting with drag objects.
* @param {boolean} [enable]
*/
C.prototype.toggleModifiers = function (enable) {
this.disabledModifiers = enable === undefined ? !this.disabledModifiers : !enable;
};
/**
* Convert px value to number.
*
* @param {String} px
* @returns {Number}
*/
var pxToNum = function (px) {
return Number(px.replace('px', ''));
};
C.MIN_SIZE = 24;
return C;
})(H5P.jQuery, H5P.EventDispatcher);
;
H5P.DragNBar = (function (EventDispatcher) {
/**
* Constructor. Initializes the drag and drop menu bar.
*
* @class
* @param {Array} buttons
* @param {H5P.jQuery} $container
* @param {H5P.jQuery} $dialogContainer
* @param {object} [options] Collection of options
* @param {boolean} [options.disableEditor=false] Determines if DragNBar should be displayed in view or editor mode
* @param {H5P.jQuery} [options.$blurHandlers] When clicking these element(s) dnb focus will be lost
*/
function DragNBar(buttons, $container, $dialogContainer, options) {
var self = this;
EventDispatcher.call(this);
this.overflowThreshold = 13; // How many buttons to display before we add the more button.
this.buttons = buttons;
this.$container = $container;
this.$dialogContainer = $dialogContainer;
this.dnd = new H5P.DragNDrop(this, $container);
this.dnd.snap = 10;
this.newElement = false;
var defaultOptions = {
disableEditor: false
};
options = H5P.jQuery.extend(defaultOptions, options);
this.isEditor = !options.disableEditor;
this.$blurHandlers = options.$blurHandlers ? options.$blurHandlers : undefined;
/**
* Keeps track of created DragNBar elements
* @type {Array}
*/
this.elements = [];
// Create a popup dialog
this.dialog = new H5P.DragNBarDialog($dialogContainer, $container);
// Fix for forcing redraw on $container, to avoid "artifcats" on safari
this.$container.addClass('hardware-accelerated');
if (this.isEditor) {
this.transformButtonActive = false;
this.initEditor();
this.initClickListeners();
H5P.$window.resize(function () {
self.resize();
});
}
}
// Inherit support for events
DragNBar.prototype = Object.create(EventDispatcher.prototype);
DragNBar.prototype.constructor = DragNBar;
return DragNBar;
})(H5P.EventDispatcher);
/**
* Initializes editor functionality of DragNBar
*/
H5P.DragNBar.prototype.initEditor = function () {
var that = this;
this.dnr = new H5P.DragNResize(this.$container);
this.dnr.snap = 10;
// Update coordinates when element is resized
this.dnr.on('moveResizing', function () {
var offset = that.$element.offset();
var position = that.$element.position();
that.updateCoordinates(offset.left, offset.top, position.left, position.top);
});
// Set pressed to not lose focus at the end of resize
this.dnr.on('stoppedResizing', function () {
that.pressed = true;
// Delete pressed after dnbelement has been refocused so it will lose focus on single click.
setTimeout(function () {
delete that.pressed;
}, 10);
});
/**
* Show transform panel listeners
*/
this.dnr.on('showTransformPanel', function () {
transformPanel(true);
});
this.dnd.on('showTransformPanel', function () {
transformPanel(true);
});
/**
* Hide transform panel listeners
*/
this.dnr.on('hideTransformPanel', function () {
if(!that.transformButtonActive) {
transformPanel(false);
}
});
this.dnd.on('hideTransformPanel', function () {
if(!that.transformButtonActive) {
transformPanel(false);
}
});
/**
* Trigger a context menu transform to either show or hide
* the transform panel.
*
* @param {boolean} show
*/
function transformPanel(show) {
if (that.focusedElement) {
that.focusedElement.contextMenu.trigger('contextMenuTransform', {showTransformPanel: show});
}
}
this.dnd.startMovingCallback = function (x, y) {
that.dnd.min = {x: 0, y: 0};
that.dnd.max = {
x: that.$container.width() - that.$element.outerWidth(),
y: that.$container.height() - that.$element.outerHeight()
};
if (that.newElement) {
that.dnd.adjust.x = 10;
that.dnd.adjust.y = 10;
that.dnd.min.y -= that.$list.height();
}
return true;
};
this.dnd.stopMovingCallback = function (event) {
var pos = {};
if (that.newElement) {
that.$container.css('overflow', '');
if (Math.round(parseFloat(that.$element.css('top'))) < 0) {
// Try to center element, but avoid overlapping
pos.x = (that.dnd.max.x / 2);
pos.y = (that.dnd.max.y / 2);
that.avoidOverlapping(pos, that.$element);
}
}
if (pos.x === undefined || pos.y === undefined ) {
pos.x = Math.round(parseFloat(that.$element.css('left')));
pos.y = Math.round(parseFloat(that.$element.css('top')));
}
that.stopMoving(pos.x, pos.y);
that.newElement = false;
delete that.dnd.min;
delete that.dnd.max;
};
};
/**
* Tries to position the given element close to the requested coordinates.
* Element can be skipped to check if spot is available.
*
* @param {object} pos
* @param {number} pos.x
* @param {number} pos.y
* @param {(H5P.jQuery|Object)} element object with width&height if ran before insertion.
*/
H5P.DragNBar.prototype.avoidOverlapping = function (pos, $element) {
// Determine size of element
var size = $element;
if (size instanceof H5P.jQuery) {
size = window.getComputedStyle(size[0]);
size = {
width: parseFloat(size.width),
height: parseFloat(size.height)
};
}
else {
$element = undefined;
}
// Determine how much they can be manuvered
var containerStyle = window.getComputedStyle(this.$container[0]);
var manX = parseFloat(containerStyle.width) - size.width;
var manY = parseFloat(containerStyle.height) - size.height;
var limit = 16;
var attempts = 0;
while (attempts < limit && this.elementOverlaps(pos.x, pos.y, $element)) {
// Try to place randomly inside container
if (manX > 0) {
pos.x = Math.floor(Math.random() * manX);
}
if (manY > 0) {
pos.y = Math.floor(Math.random() * manY);
}
attempts++;
}
};
/**
* Determine if moving the given element to its new position will cause it to
* cover another element. This can make new or pasted elements difficult to see.
* Element can be skipped to check if spot is available.
*
* @param {number} x
* @param {number} y
* @param {H5P.jQuery} [$element]
* @returns {boolean}
*/
H5P.DragNBar.prototype.elementOverlaps = function (x, y, $element) {
var self = this;
// Use snap grid
x = Math.round(x / 10);
y = Math.round(y / 10);
for (var i = 0; i < self.elements.length; i++) {
var element = self.elements[i];
if ($element !== undefined && element.$element === $element) {
continue;
}
if (x === Math.round(parseFloat(element.$element.css('left')) / 10) &&
y === Math.round(parseFloat(element.$element.css('top')) / 10)) {
return true; // Stop loop
}
}
return false;
};
// Key coordinates
var SHIFT = 16;
var CTRL = 17;
var DELETE = 46;
var C = 67;
var V = 86;
var LEFT = 37;
var UP = 38;
var RIGHT = 39;
var DOWN = 40;
// Keep track of key state
var ctrlDown = false;
var shiftDown = false;
// How many pixels to move
var snapAmount = 1;
/**
* Handle keydown events for the entire frame
*/
H5P.DragNBar.keydownHandler = function (event) {
var self = event.data.instance;
var activeElement = document.activeElement;
if (event.which === CTRL) {
ctrlDown = true;
if (self.dnd.snap !== undefined) {
// Disable snapping
delete self.dnd.snap;
}
}
if (event.which === SHIFT) {
shiftDown = true;
snapAmount = self.dnd.snap;
}
if (event.which === LEFT && self.focusedElement) {
event.preventDefault();
self.moveWithKeys(-snapAmount, 0);
}
else if (event.which === UP && self.focusedElement) {
event.preventDefault();
self.moveWithKeys(0, -snapAmount);
}
else if (event.which === RIGHT && self.focusedElement) {
event.preventDefault();
self.moveWithKeys(snapAmount, 0);
}
else if (event.which === DOWN && self.focusedElement) {
event.preventDefault();
self.moveWithKeys(0, snapAmount);
}
else if (event.which === C && ctrlDown && self.focusedElement && self.$container.is(':visible')) {
// Copy element params to clipboard
var elementSize = window.getComputedStyle(self.focusedElement.$element[0]);
var width = parseFloat(elementSize.width);
var height = parseFloat(elementSize.height) / width;
width = width / (parseFloat(window.getComputedStyle(self.$container[0]).width) / 100);
height *= width;
self.focusedElement.toClipboard(width, height);
}
else if (event.which === V && ctrlDown && window.localStorage && self.$container.is(':visible')) {
if (self.preventPaste || self.dialog.isOpen() || activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
// Don't allow paste if dialog is open or active element can be modified
return;
}
var clipboardData = localStorage.getItem('h5pClipboard');
if (clipboardData) {
// Parse
try {
clipboardData = JSON.parse(clipboardData);
}
catch (err) {
console.error('Unable to parse JSON from clipboard.', err);
return;
}
// Update file URLs
if (clipboardData.contentId !== H5PEditor.contentId) {
var prefix;
if (clipboardData.contentId) {
// Comes from existing content
if (H5PEditor.contentId) {
// .. to existing content
prefix = '../';
}
else {
// .. to new content
prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/');
}
prefix += clipboardData.contentId + '/';
}
else {
// Comes from new content
if (H5PEditor.contentId) {
// .. to existing content
prefix = (H5PEditor.editorRelUrl ? H5PEditor.editorRelUrl : '../../editor/');
}
else {
// .. to new content
prefix = '../';
}
}
H5P.DragNBar.updateFileUrls(clipboardData.specific, prefix);
}
if (clipboardData.generic) {
// Use reference instead of key
clipboardData.generic = clipboardData.specific[clipboardData.generic];
// Avoid multiple content with same ID
delete clipboardData.generic.subContentId;
}
self.trigger('paste', clipboardData);
}
}
else if ((event.which === DELETE) && self.focusedElement && self.$container.is(':visible')) {
if (activeElement.tagName.toLowerCase() === 'input') {
return;
}
else {
self.focusedElement.contextMenu.trigger('contextMenuRemove');
}
}
};
/**
* Handle keyup events for the entire frame
*/
H5P.DragNBar.keyupHandler = function (event) {
var self = event.data.instance;
if (event.which === CTRL) {
// Update key state
ctrlDown = false;
// Enable snapping
self.dnd.snap = 10;
}
if (event.which === SHIFT) {
shiftDown = false;
snapAmount = 1;
}
if (self.focusedElement && (event.which === LEFT || event.which === UP || event.which === RIGHT || event.which === DOWN)) {
// Store position of element after moving
var position = self.getElementSizeNPosition();
self.stopMoving(position.left, position.top);
}
};
/**
* Handle click events for the entire frame
*/
H5P.DragNBar.clickHandler = function (event) {
var self = event.data.instance;
// Remove pressed on click
delete self.pressed;
};
/**
* Initialize click listeners
*/
H5P.DragNBar.prototype.initClickListeners = function () {
var self = this;
// Register event listeners
var eventData = {
instance: self
};
H5P.$body.keydown(eventData, H5P.DragNBar.keydownHandler)
.keyup(eventData, H5P.DragNBar.keyupHandler)
.click(eventData, H5P.DragNBar.clickHandler);
// Set blur handler element if option has been specified
var $blurHandlers = this.$container;
if (this.$blurHandlers) {
$blurHandlers = this.$blurHandlers;
}
function handleBlur() {
// Remove coordinates picker if we didn't press an element.
if (self.pressed !== undefined) {
delete self.pressed;
}
else {
self.blurAll();
if (self.focusedElement !== undefined) {
delete self.focusedElement;
}
}
}
$blurHandlers
.keydown(function (e) {
if (e.which === 9) { // pressed tab
handleBlur();
}
})
.click(handleBlur);
};
/**
* Update file URLs. Useful when copying between different contents.
*
* @param {object} params Reference
* @param {number} contentId From source
*/
H5P.DragNBar.updateFileUrls = function (params, prefix) {
for (var prop in params) {
if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
var obj = params[prop];
if (obj.path !== undefined && obj.mime !== undefined) {
obj.path = prefix + obj.path;
}
else {
H5P.DragNBar.updateFileUrls(obj, prefix);
}
}
}
};
/**
* Attaches the menu bar to the given wrapper.
*
* @param {jQuery} $wrapper
* @returns {undefined}
*/
H5P.DragNBar.prototype.attach = function ($wrapper) {
$wrapper.html('');
$wrapper.addClass('h5peditor-dragnbar');
var $list = H5P.jQuery('
').appendTo($wrapper);
this.$list = $list;
for (var i = 0; i < this.buttons.length; i++) {
var button = this.buttons[i];
if (i === this.overflowThreshold) {
$list = H5P.jQuery('
')
.appendTo($list)
.click(function () {
return false;
})
.hover(function () {
$list.stop().slideToggle(300);
}, function () {
$list.stop().slideToggle(300);
})
.children(':first')
.next();
}
this.addButton(button, $list);
}
};
/**
* Add button.
*
* @param {type} button
* @param {Function} button.createElement Function for creating element
* @param {type} $list
* @returns {undefined}
*/
H5P.DragNBar.prototype.addButton = function (button, $list) {
var that = this;
H5P.jQuery('
')
.appendTo($list)
.children()
.click(function () {
return false;
}).mousedown(function (event) {
if (event.which !== 1) {
return;
}
that.newElement = true;
that.pressed = true;
var createdElement = button.createElement();
that.$element = createdElement;
that.$container.css('overflow', 'visible');
that.dnd.press(that.$element, event.pageX, event.pageY);
that.focus(that.$element);
});
};
/**
* Change container.
*
* @param {jQuery} $container
* @returns {undefined}
*/
H5P.DragNBar.prototype.setContainer = function ($container) {
this.$container = $container;
this.dnd.$container = $container;
};
/**
* Handler for when the dragging stops. Makes sure the element is inside its container.
*
* @param {Number} left
* @param {Number} top
* @returns {undefined}
*/
H5P.DragNBar.prototype.stopMoving = function (left, top) {
// Calculate percentage
top = top / (this.$container.height() / 100);
left = left / (this.$container.width() / 100);
this.$element.css({top: top + '%', left: left + '%'});
// Give others the result
if (this.stopMovingCallback !== undefined) {
this.stopMovingCallback(left, top);
}
};
/**
* @typedef SizeNPosition
* @type Object
* @property {number} width Outer width of the element
* @property {number} height Outer height of the element
* @property {number} left The X Coordinate
* @property {number} top The Y Coordinate
* @property {number} containerWidth Inner width of the container
* @property {number} containerHeight Inner height of the container
*/
/**
*
* Only works when element is inside this.$container. This is assumed and no
* are done.
*
* @param {H5P.jQuery} [$element] Defaults to focused element.
* @throws 'No element given' if $element is missing
* @return {SizeNPosition}
*/
H5P.DragNBar.prototype.getElementSizeNPosition = function ($element) {
$element = $element || this.focusedElement.$element;
if (!$element || !$element.length) {
throw 'No element given';
}
// Always use outer size for element
var size = $element[0].getBoundingClientRect();
// Always use position relative to container for element
var position = window.getComputedStyle($element[0]);
// We include container inner size as well
var containerSize = window.getComputedStyle(this.$container[0]);
// Start preparing return value
var sizeNPosition = {
width: parseFloat(size.width),
height: parseFloat(size.height),
left: parseFloat(position.left),
top: parseFloat(position.top),
containerWidth: parseFloat(containerSize.width),
containerHeight: parseFloat(containerSize.height)
};
if (position.left.substr(-1, 1) === '%' || position.top.substr(-1, 1) === '%') {
// Some browsers(Safari) gets percentage value instead of pixel value.
// Container inner size must be used to calculate such values.
sizeNPosition.left *= (sizeNPosition.containerWidth / 100);
sizeNPosition.top *= (sizeNPosition.containerHeight / 100);
}
return sizeNPosition;
};
/**
* Makes it possible to move dnb elements by adding to it's x and y
*
* @param {number} x Amount to move on x-axis.
* @param {number} y Amount to move on y-axis.
*/
H5P.DragNBar.prototype.moveWithKeys = function (x, y) {
/**
* Ensure that the given value is within the given boundaries.
*
* @private
* @param {number} value
* @param {number} min
* @param {number} max
* @return {number}
*/
var withinBoundaries = function (value, min, max) {
if (value < min) {
value = min;
}
if (value > max) {
value = max;
}
return value;
};
// Get size and position of current elemet in focus
var sizeNPosition = this.getElementSizeNPosition();
// Change position
sizeNPosition.left += x;
sizeNPosition.top += y;
// Check that values are within boundaries
sizeNPosition.left = withinBoundaries(sizeNPosition.left, 0, sizeNPosition.containerWidth - sizeNPosition.width);
sizeNPosition.top = withinBoundaries(sizeNPosition.top, 0, sizeNPosition.containerHeight - sizeNPosition.height);
// Determine new position style
this.$element.css({
left: sizeNPosition.left + 'px',
top: sizeNPosition.top + 'px',
});
this.dnd.trigger('showTransformPanel');
// Update position of context menu
this.updateCoordinates(sizeNPosition.left, sizeNPosition.top, sizeNPosition.left, sizeNPosition.top);
};
/**
* Makes it possible to focus and move the element around.
* Must be inside $container.
*
* @param {H5P.jQuery} $element
* @param {Object} [options]
* @param {H5P.DragNBarElement} [options.dnbElement] Register new element with dnbelement
* @param {boolean} [options.disableResize] Resize disabled
* @param {boolean} [options.lock] Lock ratio during resize
* @param {string} [clipboardData]
* @returns {H5P.DragNBarElement} Reference to added dnbelement
*/
H5P.DragNBar.prototype.add = function ($element, clipboardData, options) {
var self = this;
options = options || {};
if (this.isEditor && !options.disableResize) {
this.dnr.add($element, options);
}
var newElement = null;
// Check if element already exist
if (options.dnbElement) {
// Set element as added element
options.dnbElement.setElement($element);
newElement = options.dnbElement;
}
else {
options.element = $element;
newElement = new H5P.DragNBarElement(this, clipboardData, options);
this.elements.push(newElement);
}
$element.addClass('h5p-dragnbar-element');
if ($element.attr('tabindex') === undefined) {
// Make it possible to tab between elements.
$element.attr('tabindex', 1);
}
if (this.isEditor) {
$element.mousedown(function (event) {
if (event.which !== 1) {
return;
}
self.pressed = true;
self.focus($element);
if (self.dnr.active !== true) { // Moving can be stopped if the mousedown is doing something else
self.dnd.press($element, event.pageX, event.pageY);
}
});
}
$element.focus(function () {
self.focus($element);
});
return newElement;
};
/**
* Remove given element in the UI.
*
* @param {H5P.DragNBarElement} dnbElement
*/
H5P.DragNBar.prototype.removeElement = function (dnbElement) {
dnbElement.removeElement();
};
/**
* Select the given element in the UI.
*
* @param {jQuery} $element
* @returns {undefined}
*/
H5P.DragNBar.prototype.focus = function ($element) {
var self = this;
// Blur last focused
if (this.focusedElement && this.focusedElement.$element !== $element) {
this.focusedElement.blur();
this.focusedElement.hideContextMenu();
}
// Keep track of the element we have in focus
self.$element = $element;
this.dnd.setElement($element);
// Show and update coordinates picker
this.focusedElement = this.getDragNBarElement($element);
if (this.focusedElement) {
this.focusedElement.showContextMenu();
this.focusedElement.focus();
self.updateCoordinates();
}
// Wait for potential recreation of element
setTimeout(function () {
self.updateCoordinates();
if (self.focusedElement && self.focusedElement.contextMenu && self.focusedElement.contextMenu.canResize) {
self.focusedElement.contextMenu.updateDimensions();
}
}, 0);
};
/**
* Get dnbElement from $element
* @param {jQuery} $element
* @returns {H5P.DragNBarElement} dnbElement with matching $element
*/
H5P.DragNBar.prototype.getDragNBarElement = function ($element) {
var foundElement;
// Find object with matching element
this.elements.forEach(function (element) {
if (element.getElement().is($element)) {
foundElement = element;
}
});
return foundElement;
};
/**
* Deselect all elements in the UI.
*
* @returns {undefined}
*/
H5P.DragNBar.prototype.blurAll = function () {
this.elements.forEach(function (element) {
element.blur();
});
delete this.focusedElement;
};
/**
* Resize DnB, make sure context menu is positioned correctly.
*/
H5P.DragNBar.prototype.resize = function () {
var self = this;
this.updateCoordinates();
if (self.focusedElement) {
self.focusedElement.resizeContextMenu(self.$element.offset().left - self.$element.parent().offset().left);
}
};
/**
* Update the coordinates of context menu.
*
* @param {Number} [left]
* @param {Number} [top]
* @param {Number} [x]
* @param {Number} [y]
* @returns {undefined}
*/
H5P.DragNBar.prototype.updateCoordinates = function (left, top, x, y) {
if (!this.focusedElement) {
return;
}
var containerPosition = this.$container.position();
if (left && top && x && y) {
left = x + containerPosition.left;
top = y + containerPosition.top;
this.focusedElement.updateCoordinates(left, top, x, y);
}
else {
var position = this.$element.position();
this.focusedElement.updateCoordinates(position.left + containerPosition.left, position.top + containerPosition.top, position.left, position.top);
}
};
/**
* Creates element data to store in the clipboard.
*
* @param {string} from Source of the element
* @param {object} params Element options
* @param {string} [generic] Which part of the parameters can be used by other libraries
* @returns {string} JSON
*/
H5P.DragNBar.clipboardify = function (from, params, generic) {
var clipboardData = {
from: from,
specific: params
};
if (H5PEditor.contentId) {
clipboardData.contentId = H5PEditor.contentId;
}
// Add the generic part
if (params[generic]) {
clipboardData.generic = generic;
}
return clipboardData;
};
/**
* Make sure the given element is inside the container.
*
* @param {SizeNPosition} sizeNPosition For the element
* @returns {SizeNPosition} Only the properties which require change
*/
H5P.DragNBar.fitElementInside = function (sizeNPosition) {
var style = {};
if (sizeNPosition.left < 0) {
// Element sticks out of the left side
style.left = sizeNPosition.left = 0;
}
if (sizeNPosition.width + sizeNPosition.left > sizeNPosition.containerWidth) {
// Element sticks out of the right side
style.left = sizeNPosition.containerWidth - sizeNPosition.width;
if (style.left < 0) {
// Element is wider than the container
style.left = 0;
style.width = sizeNPosition.containerWidth;
}
}
if (sizeNPosition.top < 0) {
// Element sticks out of the top side
style.top = sizeNPosition.top = 0;
}
if (sizeNPosition.height + sizeNPosition.top > sizeNPosition.containerHeight) {
// Element sticks out of the bottom side
style.top = sizeNPosition.containerHeight - sizeNPosition.height;
if (style.top < 0) {
// Element is higher than the container
style.top = 0;
style.height = sizeNPosition.containerHeight;
}
}
return style;
};
/**
* Clean up any event listeners
*/
H5P.DragNBar.prototype.remove = function () {
H5P.$body.unbind('keydown', H5P.DragNBar.keydownHandler)
.unbind('keyup', H5P.DragNBar.keyupHandler)
.unbind('click', H5P.DragNBar.clickHandler);
};
if (window.H5PEditor) {
// Add translations
H5PEditor.language['H5P.DragNBar'] = {
libraryStrings: {
transformLabel: 'Transform',
editLabel: 'Edit',
removeLabel: 'Remove',
bringToFrontLabel: 'Bring to Front',
unableToPaste: 'Cannot paste this object. Unfortunately, the object you are trying to paste is not supported by this content type or version.',
sizeLabel: 'Size',
positionLabel: 'Position',
heightLabel: 'Height',
widthLabel: 'Width'
}
};
}
;
/*global H5P*/
/**
* Create context menu
*/
H5P.DragNBarContextMenu = (function ($, EventDispatcher) {
/**
* Constructor for context menu
* @class
* @param {jQuery} $container Parent container
* @param {H5P.DragNBarElement} DragNBarElement
* @param {boolean} [hasCoordinates] Decides if coordinates will be displayed
* @param {boolean} [disableResize] No input for dimensions
*/
function ContextMenu($container, DragNBarElement, hasCoordinates, disableResize) {
var self = this;
EventDispatcher.call(this);
/**
* Keeps track of DragNBar object
*
* @type {H5P.DragNBar}
*/
this.dnb = DragNBarElement.dnb;
/**
* Keeps track of DnBElement object
*
* @type {H5P.DragNBarElement}
*/
this.dnbElement = DragNBarElement;
/**
* Keeps track of context menu container
*
* @type {H5P.jQuery}
*/
this.$contextMenu = $('