var H5P = H5P || {};
/**
* Transition contains helper function relevant for transitioning
*/
H5P.Transition = (function ($) {
/**
* @class
* @namespace H5P
*/
Transition = {};
/**
* @private
*/
Transition.transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'transition': 'transitionend',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'msTransition': 'MSTransitionEnd'
};
/**
* @private
*/
Transition.cache = [];
/**
* Get the vendor property name for an event
*
* @function H5P.Transition.getVendorPropertyName
* @static
* @private
* @param {string} prop Generic property name
* @return {string} Vendor specific property name
*/
Transition.getVendorPropertyName = function (prop) {
if (Transition.cache[prop] !== undefined) {
return Transition.cache[prop];
}
var div = document.createElement('div');
// Handle unprefixed versions (FF16+, for example)
if (prop in div.style) {
Transition.cache[prop] = prop;
}
else {
var prefixes = ['Moz', 'Webkit', 'O', 'ms'];
var prop_ = prop.charAt(0).toUpperCase() + prop.substr(1);
if (prop in div.style) {
Transition.cache[prop] = prop;
}
else {
for (var i = 0; i < prefixes.length; ++i) {
var vendorProp = prefixes[i] + prop_;
if (vendorProp in div.style) {
Transition.cache[prop] = vendorProp;
break;
}
}
}
}
return Transition.cache[prop];
};
/**
* Get the name of the transition end event
*
* @static
* @private
* @return {string} description
*/
Transition.getTransitionEndEventName = function () {
return Transition.transitionEndEventNames[Transition.getVendorPropertyName('transition')] || undefined;
};
/**
* Helper function for listening on transition end events
*
* @function H5P.Transition.onTransitionEnd
* @static
* @param {domElement} $element The element which is transitioned
* @param {function} callback The callback to be invoked when transition is finished
* @param {number} timeout Timeout in milliseconds. Fallback if transition event is never fired
*/
Transition.onTransitionEnd = function ($element, callback, timeout) {
// Fallback on 1 second if transition event is not supported/triggered
timeout = timeout || 1000;
Transition.transitionEndEventName = Transition.transitionEndEventName || Transition.getTransitionEndEventName();
var callbackCalled = false;
var doCallback = function () {
if (callbackCalled) {
return;
}
$element.off(Transition.transitionEndEventName, callback);
callbackCalled = true;
clearTimeout(timer);
callback();
};
var timer = setTimeout(function () {
doCallback();
}, timeout);
$element.on(Transition.transitionEndEventName, function () {
doCallback();
});
};
/**
* Wait for a transition - when finished, invokes next in line
*
* @private
*
* @param {Object[]} transitions Array of transitions
* @param {H5P.jQuery} transitions[].$element Dom element transition is performed on
* @param {number=} transitions[].timeout Timeout fallback if transition end never is triggered
* @param {bool=} transitions[].break If true, sequence breaks after this transition
* @param {number} index The index for current transition
*/
var runSequence = function (transitions, index) {
if (index >= transitions.length) {
return;
}
var transition = transitions[index];
H5P.Transition.onTransitionEnd(transition.$element, function () {
if (transition.end) {
transition.end();
}
if (transition.break !== true) {
runSequence(transitions, index+1);
}
}, transition.timeout || undefined);
};
/**
* Run a sequence of transitions
*
* @function H5P.Transition.sequence
* @static
* @param {Object[]} transitions Array of transitions
* @param {H5P.jQuery} transitions[].$element Dom element transition is performed on
* @param {number=} transitions[].timeout Timeout fallback if transition end never is triggered
* @param {bool=} transitions[].break If true, sequence breaks after this transition
*/
Transition.sequence = function (transitions) {
runSequence(transitions, 0);
};
return Transition;
})(H5P.jQuery);
;
var H5P = H5P || {};
/**
* Class responsible for creating a help text dialog
*/
H5P.JoubelHelpTextDialog = (function ($) {
var numInstances = 0;
/**
* Display a pop-up containing a message.
*
* @param {H5P.jQuery} $container The container which message dialog will be appended to
* @param {string} message The message
* @param {string} closeButtonTitle The title for the close button
* @return {H5P.jQuery}
*/
function JoubelHelpTextDialog(header, message, closeButtonTitle) {
H5P.EventDispatcher.call(this);
var self = this;
numInstances++;
var headerId = 'joubel-help-text-header-' + numInstances;
var helpTextId = 'joubel-help-text-body-' + numInstances;
var $helpTextDialogBox = $('
'
).append([$tail, $innerBubble])
.appendTo($h5pContainer);
// Show speech bubble with transition
setTimeout(function () {
$currentSpeechBubble.addClass('show');
}, 0);
position($currentSpeechBubble, $currentContainer, maxWidth, $tail, $innerTail);
// Handle click to close
H5P.$body.on('mousedown.speechBubble', handleOutsideClick);
// Handle window resizing
H5P.$window.on('resize', '', handleResize);
// Handle clicks when inside IV which blocks bubbling.
$container.parents('.h5p-dialog')
.on('mousedown.speechBubble', handleOutsideClick);
if (iDevice) {
H5P.$body.css('cursor', 'pointer');
}
return this;
}
// Remove speechbubble if it belongs to a dom element that is about to be hidden
H5P.externalDispatcher.on('domHidden', function (event) {
if ($currentSpeechBubble !== undefined && event.data.$dom.find($currentContainer).length !== 0) {
remove();
}
});
/**
* Returns the closest h5p container for the given DOM element.
*
* @param {object} $container jquery element
* @return {object} the h5p container (jquery element)
*/
function getH5PContainer($container) {
var $h5pContainer = $container.closest('.h5p-frame');
// Check closest h5p frame first, then check for container in case there is no frame.
if (!$h5pContainer.length) {
$h5pContainer = $container.closest('.h5p-container');
}
return $h5pContainer;
}
/**
* Event handler that is called when the window is resized.
*/
function handleResize() {
position($currentSpeechBubble, $currentContainer, currentMaxWidth, $tail, $innerTail);
}
/**
* Repositions the speech bubble according to the position of the container.
*
* @param {object} $currentSpeechbubble the speech bubble that should be positioned
* @param {object} $container the container to which the speech bubble should point
* @param {number} maxWidth the maximum width of the speech bubble
* @param {object} $tail the tail (the triangle that points to the referenced container)
* @param {object} $innerTail the inner tail (the triangle that points to the referenced container)
*/
function position($currentSpeechBubble, $container, maxWidth, $tail, $innerTail) {
var $h5pContainer = getH5PContainer($container);
// Calculate offset between the button and the h5p frame
var offset = getOffsetBetween($h5pContainer, $container);
var direction = (offset.bottom > offset.top ? 'bottom' : 'top');
var tipWidth = offset.outerWidth * 0.9; // Var needs to be renamed to make sense
var bubbleWidth = tipWidth > maxWidth ? maxWidth : tipWidth;
var bubblePosition = getBubblePosition(bubbleWidth, offset);
var tailPosition = getTailPosition(bubbleWidth, bubblePosition, offset, $container.width());
// Need to set font-size, since element is appended to body.
// Using same font-size as parent. In that way it will grow accordingly
// when resizing
var fontSize = 16;//parseFloat($parent.css('font-size'));
// Set width and position of speech bubble
$currentSpeechBubble.css(bubbleCSS(
direction,
bubbleWidth,
bubblePosition,
fontSize
));
var preparedTailCSS = tailCSS(direction, tailPosition);
$tail.css(preparedTailCSS);
$innerTail.css(preparedTailCSS);
}
/**
* Static function for removing the speechbubble
*/
var remove = function () {
H5P.$body.off('mousedown.speechBubble');
H5P.$window.off('resize', '', handleResize);
$currentContainer.parents('.h5p-dialog').off('mousedown.speechBubble');
if (iDevice) {
H5P.$body.css('cursor', '');
}
if ($currentSpeechBubble !== undefined) {
// Apply transition, then remove speech bubble
$currentSpeechBubble.removeClass('show');
// Make sure we remove any old timeout before reassignment
clearTimeout(removeSpeechBubbleTimeout);
removeSpeechBubbleTimeout = setTimeout(function () {
$currentSpeechBubble.remove();
$currentSpeechBubble = undefined;
}, 500);
}
// Don't return false here. If the user e.g. clicks a button when the bubble is visible,
// we want the bubble to disapear AND the button to receive the event
};
/**
* Remove the speech bubble and container reference
*/
function handleOutsideClick(event) {
if (event.target === $currentContainer[0]) {
return; // Button clicks are not outside clicks
}
remove();
// There is no current container when a container isn't clicked
$currentContainer = undefined;
}
/**
* Calculate position for speech bubble
*
* @param {number} bubbleWidth The width of the speech bubble
* @param {object} offset
* @return {object} Return position for the speech bubble
*/
function getBubblePosition(bubbleWidth, offset) {
var bubblePosition = {};
var tailOffset = 9;
var widthOffset = bubbleWidth / 2;
// Calculate top position
bubblePosition.top = offset.top + offset.innerHeight;
// Calculate bottom position
bubblePosition.bottom = offset.bottom + offset.innerHeight + tailOffset;
// Calculate left position
if (offset.left < widthOffset) {
bubblePosition.left = 3;
}
else if ((offset.left + widthOffset) > offset.outerWidth) {
bubblePosition.left = offset.outerWidth - bubbleWidth - 3;
}
else {
bubblePosition.left = offset.left - widthOffset + (offset.innerWidth / 2);
}
return bubblePosition;
}
/**
* Calculate position for speech bubble tail
*
* @param {number} bubbleWidth The width of the speech bubble
* @param {object} bubblePosition Speech bubble position
* @param {object} offset
* @param {number} iconWidth The width of the tip icon
* @return {object} Return position for the tail
*/
function getTailPosition(bubbleWidth, bubblePosition, offset, iconWidth) {
var tailPosition = {};
// Magic numbers. Tuned by hand so that the tail fits visually within
// the bounds of the speech bubble.
var leftBoundary = 9;
var rightBoundary = bubbleWidth - 20;
tailPosition.left = offset.left - bubblePosition.left + (iconWidth / 2) - 6;
if (tailPosition.left < leftBoundary) {
tailPosition.left = leftBoundary;
}
if (tailPosition.left > rightBoundary) {
tailPosition.left = rightBoundary;
}
tailPosition.top = -6;
tailPosition.bottom = -6;
return tailPosition;
}
/**
* Return bubble CSS for the desired growth direction
*
* @param {string} direction The direction the speech bubble will grow
* @param {number} width The width of the speech bubble
* @param {object} position Speech bubble position
* @param {number} fontSize The size of the bubbles font
* @return {object} Return CSS
*/
function bubbleCSS(direction, width, position, fontSize) {
if (direction === 'top') {
return {
width: width + 'px',
bottom: position.bottom + 'px',
left: position.left + 'px',
fontSize: fontSize + 'px',
top: ''
};
}
else {
return {
width: width + 'px',
top: position.top + 'px',
left: position.left + 'px',
fontSize: fontSize + 'px',
bottom: ''
};
}
}
/**
* Return tail CSS for the desired growth direction
*
* @param {string} direction The direction the speech bubble will grow
* @param {object} position Tail position
* @return {object} Return CSS
*/
function tailCSS(direction, position) {
if (direction === 'top') {
return {
bottom: position.bottom + 'px',
left: position.left + 'px',
top: ''
};
}
else {
return {
top: position.top + 'px',
left: position.left + 'px',
bottom: ''
};
}
}
/**
* Calculates the offset between an element inside a container and the
* container. Only works if all the edges of the inner element are inside the
* outer element.
* Width/height of the elements is included as a convenience.
*
* @param {H5P.jQuery} $outer
* @param {H5P.jQuery} $inner
* @return {object} Position offset
*/
function getOffsetBetween($outer, $inner) {
var outer = $outer[0].getBoundingClientRect();
var inner = $inner[0].getBoundingClientRect();
return {
top: inner.top - outer.top,
right: outer.right - inner.right,
bottom: outer.bottom - inner.bottom,
left: inner.left - outer.left,
innerWidth: inner.width,
innerHeight: inner.height,
outerWidth: outer.width,
outerHeight: outer.height
};
}
return JoubelSpeechBubble;
})(H5P.jQuery);
;
var H5P = H5P || {};
H5P.JoubelThrobber = (function ($) {
/**
* Creates a new tip
*/
function JoubelThrobber() {
// h5p-throbber css is described in core
var $throbber = $('', {
'class': 'h5p-throbber'
});
return $throbber;
}
return JoubelThrobber;
}(H5P.jQuery));
;
H5P.JoubelTip = (function ($) {
var $conv = $('');
/**
* Creates a new tip element.
*
* NOTE that this may look like a class but it doesn't behave like one.
* It returns a jQuery object.
*
* @param {string} tipHtml The text to display in the popup
* @param {Object} [behaviour] Options
* @param {string} [behaviour.tipLabel] Set to use a custom label for the tip button (you want this for good A11Y)
* @param {boolean} [behaviour.helpIcon] Set to 'true' to Add help-icon classname to Tip button (changes the icon)
* @param {boolean} [behaviour.showSpeechBubble] Set to 'false' to disable functionality (you may this in the editor)
* @param {boolean} [behaviour.tabcontrol] Set to 'true' if you plan on controlling the tabindex in the parent (tabindex="-1")
* @return {H5P.jQuery|undefined} Tip button jQuery element or 'undefined' if invalid tip
*/
function JoubelTip(tipHtml, behaviour) {
// Keep track of the popup that appears when you click the Tip button
var speechBubble;
// Parse tip html to determine text
var tipText = $conv.html(tipHtml).text().trim();
if (tipText === '') {
return; // The tip has no textual content, i.e. it's invalid.
}
// Set default behaviour
behaviour = $.extend({
tipLabel: tipText,
helpIcon: false,
showSpeechBubble: true,
tabcontrol: false
}, behaviour);
// Create Tip button
var $tipButton = $('', {
class: 'joubel-tip-container' + (behaviour.showSpeechBubble ? '' : ' be-quiet'),
'aria-label': behaviour.tipLabel,
'aria-expanded': false,
role: 'button',
tabindex: (behaviour.tabcontrol ? -1 : 0),
click: function (event) {
// Toggle show/hide popup
toggleSpeechBubble();
event.preventDefault();
},
keydown: function (event) {
if (event.which === 32 || event.which === 13) { // Space & enter key
// Toggle show/hide popup
toggleSpeechBubble();
event.stopPropagation();
event.preventDefault();
}
else { // Any other key
// Toggle hide popup
toggleSpeechBubble(false);
}
},
// Add markup to render icon
html: '' +
'' +
'' +
'' +
''
// IMPORTANT: All of the markup elements must have 'pointer-events: none;'
});
const $tipAnnouncer = $('
', {
'class': 'hidden-but-read',
'aria-live': 'polite',
appendTo: $tipButton,
});
/**
* Tip button interaction handler.
* Toggle show or hide the speech bubble popup when interacting with the
* Tip button.
*
* @private
* @param {boolean} [force] 'true' shows and 'false' hides.
*/
var toggleSpeechBubble = function (force) {
if (speechBubble !== undefined && speechBubble.isCurrent($tipButton)) {
// Hide current popup
speechBubble.remove();
speechBubble = undefined;
$tipButton.attr('aria-expanded', false);
$tipAnnouncer.html('');
}
else if (force !== false && behaviour.showSpeechBubble) {
// Create and show new popup
speechBubble = H5P.JoubelSpeechBubble($tipButton, tipHtml);
$tipButton.attr('aria-expanded', true);
$tipAnnouncer.html(tipHtml);
}
};
return $tipButton;
}
return JoubelTip;
})(H5P.jQuery);
;
var H5P = H5P || {};
H5P.JoubelSlider = (function ($) {
/**
* Creates a new Slider
*
* @param {object} [params] Additional parameters
*/
function JoubelSlider(params) {
H5P.EventDispatcher.call(this);
this.$slider = $('
', $.extend({
'class': 'h5p-joubel-ui-slider'
}, params));
this.$slides = [];
this.currentIndex = 0;
this.numSlides = 0;
}
JoubelSlider.prototype = Object.create(H5P.EventDispatcher.prototype);
JoubelSlider.prototype.constructor = JoubelSlider;
JoubelSlider.prototype.addSlide = function ($content) {
$content.addClass('h5p-joubel-ui-slide').css({
'left': (this.numSlides*100) + '%'
});
this.$slider.append($content);
this.$slides.push($content);
this.numSlides++;
if(this.numSlides === 1) {
$content.addClass('current');
}
};
JoubelSlider.prototype.attach = function ($container) {
$container.append(this.$slider);
};
JoubelSlider.prototype.move = function (index) {
var self = this;
if(index === 0) {
self.trigger('first-slide');
}
if(index+1 === self.numSlides) {
self.trigger('last-slide');
}
self.trigger('move');
var $previousSlide = self.$slides[this.currentIndex];
H5P.Transition.onTransitionEnd(this.$slider, function () {
$previousSlide.removeClass('current');
self.trigger('moved');
});
this.$slides[index].addClass('current');
var translateX = 'translateX(' + (-index*100) + '%)';
this.$slider.css({
'-webkit-transform': translateX,
'-moz-transform': translateX,
'-ms-transform': translateX,
'transform': translateX
});
this.currentIndex = index;
};
JoubelSlider.prototype.remove = function () {
this.$slider.remove();
};
JoubelSlider.prototype.next = function () {
if(this.currentIndex+1 >= this.numSlides) {
return;
}
this.move(this.currentIndex+1);
};
JoubelSlider.prototype.previous = function () {
this.move(this.currentIndex-1);
};
JoubelSlider.prototype.first = function () {
this.move(0);
};
JoubelSlider.prototype.last = function () {
this.move(this.numSlides-1);
};
return JoubelSlider;
})(H5P.jQuery);
;
var H5P = H5P || {};
/**
* @module
*/
H5P.JoubelScoreBar = (function ($) {
/* Need to use an id for the star SVG since that is the only way to reference
SVG filters */
var idCounter = 0;
/**
* Creates a score bar
* @class H5P.JoubelScoreBar
* @param {number} maxScore Maximum score
* @param {string} [label] Makes it easier for readspeakers to identify the scorebar
* @param {string} [helpText] Score explanation
* @param {string} [scoreExplanationButtonLabel] Label for score explanation button
*/
function JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel) {
var self = this;
self.maxScore = maxScore;
self.score = 0;
idCounter++;
/**
* @const {string}
*/
self.STAR_MARKUP = '';
/**
* @function appendTo
* @memberOf H5P.JoubelScoreBar#
* @param {H5P.jQuery} $wrapper Dom container
*/
self.appendTo = function ($wrapper) {
self.$scoreBar.appendTo($wrapper);
};
/**
* Create the text representation of the scorebar .
*
* @private
* @return {string}
*/
var createLabel = function (score) {
if (!label) {
return '';
}
return label.replace(':num', score).replace(':total', self.maxScore);
};
/**
* Creates the html for this widget
*
* @method createHtml
* @private
*/
var createHtml = function () {
// Container div
self.$scoreBar = $('
', {
'class': 'h5p-joubelui-score-bar',
});
var $visuals = $('
', {
'class': 'h5p-joubelui-score-bar-visuals',
appendTo: self.$scoreBar
});
// The progress bar wrapper
self.$progressWrapper = $('
', {
'class': 'h5p-joubelui-progressbar-background'
}).appendTo(this.$progressbar);
}
JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype);
JoubelProgressbar.prototype.constructor = JoubelProgressbar;
JoubelProgressbar.prototype.updateAria = function () {
var self = this;
if (this.options.disableAria) {
return;
}
if (!this.$currentStatus) {
this.$currentStatus = $('
', {
'class': 'h5p-joubelui-progressbar-slide-status-text',
'aria-live': 'assertive'
}).appendTo(this.$progressbar);
}
var interpolatedProgressText = self.options.progressText
.replace(':num', self.currentStep)
.replace(':total', self.steps);
this.$currentStatus.html(interpolatedProgressText);
};
/**
* Appends to a container
* @method appendTo
* @param {H5P.jquery} $container
*/
JoubelProgressbar.prototype.appendTo = function ($container) {
this.$progressbar.appendTo($container);
};
/**
* Update progress
* @method setProgress
* @param {number} step
*/
JoubelProgressbar.prototype.setProgress = function (step) {
// Check for valid value:
if (step > this.steps || step < 0) {
return;
}
this.currentStep = step;
this.$background.css({
width: ((this.currentStep/this.steps)*100) + '%'
});
this.updateAria();
};
/**
* Increment progress with 1
* @method next
*/
JoubelProgressbar.prototype.next = function () {
this.setProgress(this.currentStep+1);
};
/**
* Reset progressbar
* @method reset
*/
JoubelProgressbar.prototype.reset = function () {
this.setProgress(0);
};
/**
* Check if last step is reached
* @method isLastStep
* @return {Boolean}
*/
JoubelProgressbar.prototype.isLastStep = function () {
return this.steps === this.currentStep;
};
return JoubelProgressbar;
})(H5P.jQuery);
;
var H5P = H5P || {};
/**
* H5P Joubel UI library.
*
* This is a utility library, which does not implement attach. I.e, it has to bee actively used by
* other libraries
* @module
*/
H5P.JoubelUI = (function ($) {
/**
* The internal object to return
* @class H5P.JoubelUI
* @static
*/
function JoubelUI() {}
/* Public static functions */
/**
* Create a tip icon
* @method H5P.JoubelUI.createTip
* @param {string} text The textual tip
* @param {Object} params Parameters
* @return {H5P.JoubelTip}
*/
JoubelUI.createTip = function (text, params) {
return new H5P.JoubelTip(text, params);
};
/**
* Create message dialog
* @method H5P.JoubelUI.createMessageDialog
* @param {H5P.jQuery} $container The dom container
* @param {string} message The message
* @return {H5P.JoubelMessageDialog}
*/
JoubelUI.createMessageDialog = function ($container, message) {
return new H5P.JoubelMessageDialog($container, message);
};
/**
* Create help text dialog
* @method H5P.JoubelUI.createHelpTextDialog
* @param {string} header The textual header
* @param {string} message The textual message
* @param {string} closeButtonTitle The title for the close button
* @return {H5P.JoubelHelpTextDialog}
*/
JoubelUI.createHelpTextDialog = function (header, message, closeButtonTitle) {
return new H5P.JoubelHelpTextDialog(header, message, closeButtonTitle);
};
/**
* Create progress circle
* @method H5P.JoubelUI.createProgressCircle
* @param {number} number The progress (0 to 100)
* @param {string} progressColor The progress color in hex value
* @param {string} fillColor The fill color in hex value
* @param {string} backgroundColor The background color in hex value
* @return {H5P.JoubelProgressCircle}
*/
JoubelUI.createProgressCircle = function (number, progressColor, fillColor, backgroundColor) {
return new H5P.JoubelProgressCircle(number, progressColor, fillColor, backgroundColor);
};
/**
* Create throbber for loading
* @method H5P.JoubelUI.createThrobber
* @return {H5P.JoubelThrobber}
*/
JoubelUI.createThrobber = function () {
return new H5P.JoubelThrobber();
};
/**
* Create simple rounded button
* @method H5P.JoubelUI.createSimpleRoundedButton
* @param {string} text The button label
* @return {H5P.SimpleRoundedButton}
*/
JoubelUI.createSimpleRoundedButton = function (text) {
return new H5P.SimpleRoundedButton(text);
};
/**
* Create Slider
* @method H5P.JoubelUI.createSlider
* @param {Object} [params] Parameters
* @return {H5P.JoubelSlider}
*/
JoubelUI.createSlider = function (params) {
return new H5P.JoubelSlider(params);
};
/**
* Create Score Bar
* @method H5P.JoubelUI.createScoreBar
* @param {number=} maxScore The maximum score
* @param {string} [label] Makes it easier for readspeakers to identify the scorebar
* @return {H5P.JoubelScoreBar}
*/
JoubelUI.createScoreBar = function (maxScore, label, helpText, scoreExplanationButtonLabel) {
return new H5P.JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel);
};
/**
* Create Progressbar
* @method H5P.JoubelUI.createProgressbar
* @param {number=} numSteps The total numer of steps
* @param {Object} [options] Additional options
* @param {boolean} [options.disableAria] Disable readspeaker assistance
* @param {string} [options.progressText] A progress text for describing
* current progress out of total progress for readspeakers.
* e.g. "Slide :num of :total"
* @return {H5P.JoubelProgressbar}
*/
JoubelUI.createProgressbar = function (numSteps, options) {
return new H5P.JoubelProgressbar(numSteps, options);
};
/**
* Create standard Joubel button
*
* @method H5P.JoubelUI.createButton
* @param {object} params
* May hold any properties allowed by jQuery. If href is set, an A tag
* is used, if not a button tag is used.
* @return {H5P.jQuery} The jquery element created
*/
JoubelUI.createButton = function(params) {
var type = 'button';
if (params.href) {
type = 'a';
}
else {
params.type = 'button';
}
if (params.class) {
params.class += ' h5p-joubelui-button';
}
else {
params.class = 'h5p-joubelui-button';
}
return $('<' + type + '/>', params);
};
/**
* Fix for iframe scoll bug in IOS. When focusing an element that doesn't have
* focus support by default the iframe will scroll the parent frame so that
* the focused element is out of view. This varies dependening on the elements
* of the parent frame.
*/
if (H5P.isFramed && !H5P.hasiOSiframeScrollFix &&
/iPad|iPhone|iPod/.test(navigator.userAgent)) {
H5P.hasiOSiframeScrollFix = true;
// Keep track of original focus function
var focus = HTMLElement.prototype.focus;
// Override the original focus
HTMLElement.prototype.focus = function () {
// Only focus the element if it supports it natively
if ( (this instanceof HTMLAnchorElement ||
this instanceof HTMLInputElement ||
this instanceof HTMLSelectElement ||
this instanceof HTMLTextAreaElement ||
this instanceof HTMLButtonElement ||
this instanceof HTMLIFrameElement ||
this instanceof HTMLAreaElement) && // HTMLAreaElement isn't supported by Safari yet.
!this.getAttribute('role')) { // Focus breaks if a different role has been set
// In theory this.isContentEditable should be able to recieve focus,
// but it didn't work when tested.
// Trigger the original focus with the proper context
focus.call(this);
}
};
}
return JoubelUI;
})(H5P.jQuery);
;
if (!Math.sign) {
Math.sign = function(x) {
// If x is NaN, the result is NaN.
// If x is -0, the result is -0.
// If x is +0, the result is +0.
// If x is negative and not -0, the result is -1.
// If x is positive and not +0, the result is +1.
return ((x > 0) - (x < 0)) || +x;
// A more aesthetic pseudo-representation:
//
// ( (x > 0) ? 1 : 0 ) // if x is positive, then positive one
// + // else (because you can't be both - and +)
// ( (x < 0) ? -1 : 0 ) // if x is negative, then negative one
// || // if x is 0, -0, or NaN, or not a number,
// +x // then the result will be x, (or) if x is
// // not a number, then x converts to number
};
}
(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.algebra=e()}})(function(){var e,t,r;return function n(e,t,r){function i(o,a){if(!t[o]){if(!e[o]){var u=typeof require=="function"&&require;if(!a&&u)return u(o,!0);if(s)return s(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=t[o]={exports:{}};e[o][0].call(l.exports,function(t){var r=e[o][1][t];return i(r?r:t)},l,l.exports,n,e,t,r)}return t[o].exports}var s=typeof require=="function"&&require;for(var o=0;o=0){if(v.valueOf()===0){return[p.multiply(-1).divide(c.multiply(2)).reduce()]}else{var m;if(v._squareRootIsRational()){m=v.pow(.5);var d=p.multiply(-1).subtract(m).divide(c.multiply(2));var y=p.multiply(-1).add(m).divide(c.multiply(2));return[d.reduce(),y.reduce()]}else{m=Math.sqrt(v.valueOf());c=c.valueOf();p=p.valueOf();var d=(-p-m)/(2*c);var y=(-p+m)/(2*c);return[d,y]}}}else{return[]}}else if(this._isCubic(e)){var l=r._cubicCoefficients();var c=l.a;var p=l.b;var h=l.c;var g=l.d;var b=c.multiply(p).multiply(h).multiply(g).multiply(18);b=b.subtract(p.pow(3).multiply(g).multiply(4));b=b.add(p.pow(2).multiply(h.pow(2)));b=b.subtract(c.multiply(h.pow(3)).multiply(4));b=b.subtract(c.pow(2).multiply(g.pow(2)).multiply(27));var w=p.pow(2).subtract(c.multiply(h).multiply(3));if(b.valueOf()===0){if(w.valueOf()===0){var d=p.multiply(-1).divide(c.multiply(3));return[d.reduce()]}else{var d=c.multiply(p).multiply(h).multiply(4);d=d.subtract(c.pow(2).multiply(g).multiply(9));d=d.subtract(p.pow(3));d=d.divide(c.multiply(w));var y=c.multiply(g).multiply(9).subtract(p.multiply(h)).divide(w.multiply(2));return[d.reduce(),y.reduce()]}}else{var _=(3*(h/c)-Math.pow(p,2)/Math.pow(c,2))/3;var T=2*Math.pow(p,3)/Math.pow(c,3);T=T-9*p*h/Math.pow(c,2);T=T+27*g/c;T=T/27;var E=Math.pow(T,2)/4+Math.pow(_,3)/27;if(E>0){var x=-(T/2)+Math.sqrt(E);var S=Math.cbrt(x);var I=-(T/2)-Math.sqrt(E);var M=Math.cbrt(I);var d=S+M-p/(3*c);if(d<0){var R=Math.floor(d);if(d-R<1e-15)d=R}else if(d>0){var R=Math.ceil(d);if(R-d<1e-15)d=R}return[d]}else{var u=Math.sqrt(Math.pow(T,2)/4-E);var A=Math.cbrt(u);var O=Math.acos(-(T/(2*u)));var D=-A;var k=Math.cos(O/3);var P=Math.sqrt(3)*Math.sin(O/3);var V=-(p/(3*c));var d=2*A*Math.cos(O/3)-p/(3*c);var y=D*(k+P)+V;var C=D*(k-P)+V;if(d<0){var R=Math.floor(d);if(d-R<1e-15)d=R}else if(d>0){var R=Math.ceil(d);if(R-d<1e-15)d=R}if(y<0){var F=Math.floor(y);if(y-F<1e-15)y=F}else if(y>0){var F=Math.ceil(y);if(F-y<1e-15)y=F}if(d<0){var q=Math.floor(C);if(C-q<1e-15)C=q}else if(C>0){var q=Math.ceil(C);if(q-C<1e-15)C=q}var N=[d,y,C];N.sort(function(e,t){return e-t});return[N[0],N[1],N[2]]}}}}};u.prototype.eval=function(e){return new u(this.lhs.eval(e),this.rhs.eval(e))};u.prototype.toString=function(){return this.lhs.toString()+" = "+this.rhs.toString()};u.prototype.toTex=function(){return this.lhs.toTex()+" = "+this.rhs.toTex()};u.prototype._maxDegree=function(){var e=this.lhs._maxDegree();var t=this.rhs._maxDegree();return Math.max(e,t)};u.prototype._maxDegreeOfVariable=function(e){return Math.max(this.lhs._maxDegreeOfVariable(e),this.rhs._maxDegreeOfVariable(e))};u.prototype._variableCanBeIsolated=function(e){return this._maxDegreeOfVariable(e)===1&&this._noCrossProductsWithVariable(e)};u.prototype._noCrossProductsWithVariable=function(e){return this.lhs._noCrossProductsWithVariable(e)&&this.rhs._noCrossProductsWithVariable(e)};u.prototype._noCrossProducts=function(){return this.lhs._noCrossProducts()&&this.rhs._noCrossProducts()};u.prototype._onlyHasVariable=function(e){return this.lhs._onlyHasVariable(e)&&this.rhs._onlyHasVariable(e)};u.prototype._isLinear=function(){return this._maxDegree()===1&&this._noCrossProducts()};u.prototype._isQuadratic=function(e){return this._maxDegree()===2&&this._onlyHasVariable(e)};u.prototype._isCubic=function(e){return this._maxDegree()===3&&this._onlyHasVariable(e)};t.exports=u},{"./expressions":3,"./fractions":4,"./helper":5}],3:[function(e,t,r){var n=e("./fractions");var i=e("./helper").isInt;var s=e("./helper").GREEK_LETTERS;var o=function(e){this.constants=[];if(typeof e==="string"){var t=new a(e);var r=new Term(t);this.terms=[r]}else if(i(e)){this.constants=[new n(e,1)];this.terms=[]}else if(e instanceof n){this.constants=[e];this.terms=[]}else if(e instanceof Term){this.terms=[e]}else if(typeof e==="undefined"){this.terms=[]}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Argument must be of type String, Integer, Fraction or Term.")}};o.prototype.constant=function(){return this.constants.reduce(function(e,t){return e.add(t)},new n(0,1))};o.prototype.simplify=function(){var e=this.copy();e.terms=e.terms.map(function(e){return e.simplify()});e._sort();e._combineLikeTerms();e._moveTermsWithDegreeZeroToConstants();e._removeTermsWithCoefficientZero();e.constants=e.constant().valueOf()===0?[]:[e.constant()];return e};o.prototype.copy=function(){var e=new o;e.constants=this.constants.map(function(e){return e.copy()});e.terms=this.terms.map(function(e){return e.copy()});return e};o.prototype.add=function(e,t){var r=this.copy();if(typeof e==="string"||e instanceof Term||i(e)||e instanceof n){var s=new o(e);return r.add(s,t)}else if(e instanceof o){var a=e.copy().terms;r.terms=r.terms.concat(a);r.constants=r.constants.concat(e.constants);r._sort()}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Summand must be of type String, Expression, Term, Fraction or Integer.")}return t||t===undefined?r.simplify():r};o.prototype.subtract=function(e,t){var r=e instanceof o?e.multiply(-1):new o(e).multiply(-1);return this.add(r,t)};o.prototype.multiply=function(e,t){var r=this.copy();if(typeof e==="string"||e instanceof Term||i(e)||e instanceof n){var s=new o(e);return r.multiply(s,t)}else if(e instanceof o){var a=e.copy();var u=[];for(var f=0;f1){return false}}return true};o.prototype._maxDegree=function(){return this.terms.reduce(function(e,t){return Math.max(e,t.maxDegree())},1)};o.prototype._maxDegreeOfVariable=function(e){return this.terms.reduce(function(t,r){return Math.max(t,r.maxDegreeOfVariable(e))},1)};o.prototype._quadraticCoefficients=function(){var e;var t=new n(0,1);for(var r=0;r-1){t="\\"+t}if(e===0){return""}else if(e===1){return t}else{return t+"^{"+e+"}"}};t.exports={Expression:o,Term:Term,Variable:a}},{"./fractions":4,"./helper":5}],4:[function(e,t,r){var n=e("./helper").isInt;var i=e("./helper").gcd;var s=e("./helper").lcm;var o=function(e,t){if(t===0){throw new EvalError("Divide By Zero")}else if(n(e)&&n(t)){this.numer=e;this.denom=t}else{throw new TypeError("Invalid Argument ("+e.toString()+","+t.toString()+"): Divisor and dividend must be of type Integer.")}};o.prototype.copy=function(){return new o(this.numer,this.denom)};o.prototype.reduce=function(){var e=this.copy();var t=i(e.numer,e.denom);e.numer=e.numer/t;e.denom=e.denom/t;if(Math.sign(e.denom)==-1&&Math.sign(e.numer)==1){e.numer*=-1;e.denom*=-1}return e};o.prototype.equalTo=function(e){if(e instanceof o){var t=this.reduce();var r=e.reduce();return t.numer===r.numer&&t.denom===r.denom}else{return false}};o.prototype.add=function(e,t){t=t===undefined?true:t;var r,i;if(e instanceof o){r=e.numer;i=e.denom}else if(n(e)){r=e;i=1}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Summand must be of type Fraction or Integer.")}var a=this.copy();if(this.denom==i){a.numer+=r}else{var u=s(a.denom,i);var f=u/a.denom;var l=u/i;a.numer*=f;a.denom*=f;r*=l;a.numer+=r}return t?a.reduce():a};o.prototype.subtract=function(e,t){t=t===undefined?true:t;var r=this.copy();if(e instanceof o){return r.add(new o(-e.numer,e.denom),t)}else if(n(e)){return r.add(new o(-e,1),t)}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Subtrahend must be of type Fraction or Integer.")}};o.prototype.multiply=function(e,t){t=t===undefined?true:t;var r,i;if(e instanceof o){r=e.numer;i=e.denom}else if(n(e)&&e){r=e;i=1}else if(e===0){r=0;i=1}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Multiplicand must be of type Fraction or Integer.")}var s=this.copy();s.numer*=r;s.denom*=i;return t?s.reduce():s};o.prototype.divide=function(e,t){t=t===undefined?true:t;if(e.valueOf()===0){throw new EvalError("Divide By Zero")}var r=this.copy();if(e instanceof o){return r.multiply(new o(e.denom,e.numer),t)}else if(n(e)){return r.multiply(new o(1,e),t)}else{throw new TypeError("Invalid Argument ("+e.toString()+"): Divisor must be of type Fraction or Integer.")}};o.prototype.pow=function(e,t){t=t===undefined?true:t;var r=this.copy();r.numer=Math.pow(r.numer,e);r.denom=Math.pow(r.denom,e);return t?r.reduce():r};o.prototype.abs=function(){var e=this.copy();e.numer=Math.abs(e.numer);e.denom=Math.abs(e.denom);return e};o.prototype.valueOf=function(){return this.numer/this.denom};o.prototype.toString=function(){if(this.numer===0){return"0"}else if(this.denom===1){return this.numer.toString()}else if(this.denom===-1){return(-this.numer).toString()}else{return this.numer+"/"+this.denom}};o.prototype.toTex=function(){if(this.numer===0){return"0"}else if(this.denom===1){return this.numer.toString()}else if(this.denom===-1){return(-this.numer).toString()}else{return"\\frac{"+this.numer+"}{"+this.denom+"}"}};o.prototype._squareRootIsRational=function(){if(this.valueOf()===0){return true}var e=Math.sqrt(this.numer);var t=Math.sqrt(this.denom);return n(e)&&n(t)};o.prototype._cubeRootIsRational=function(){if(this.valueOf()===0){return true}var e=Math.cbrt(this.numer);var t=Math.cbrt(this.denom);return n(e)&&n(t)};t.exports=o},{"./helper":5}],5:[function(e,t,r){function n(e,t){while(t){var r=e;e=t;t=r%t}return e}function i(e,t){return e*t/n(e,t)}function s(e){return typeof e=="number"&&e%1===0}function o(e,t){t=typeof t==="undefined"?2:t;var r=Math.pow(10,t);return Math.round(parseFloat(e)*r)/r}var a=["alpha","beta","gamma","Gamma","delta","Delta","epsilon","varepsilon","zeta","eta","theta","vartheta","Theta","iota","kappa","lambda","Lambda","mu","nu","xi","Xi","pi","Pi","rho","varrho","sigma","Sigma","tau","upsilon","Upsilon","phi","varphi","Phi","chi","psi","Psi","omega","Omega"];r.gcd=n;r.lcm=i;r.isInt=s;r.round=o;r.GREEK_LETTERS=a},{}],6:[function(e,t,r){"use strict";var n=function(){this.pos=0;this.buf=null;this.buflen=0;this.optable={"+":"PLUS","-":"MINUS","*":"MULTIPLY","/":"DIVIDE","^":"POWER","(":"L_PAREN",")":"R_PAREN","=":"EQUALS"}};n.prototype.input=function(e){this.pos=0;this.buf=e;this.buflen=e.length};n.prototype.token=function(){this._skipnontokens();if(this.pos>=this.buflen){return null}var e=this.buf.charAt(this.pos);var t=this.optable[e];if(t!==undefined){if(t==="L_PAREN"||t==="R_PAREN"){return{type:"PAREN",value:t,pos:this.pos++}}else{return{type:"OPERATOR",value:t,pos:this.pos++}}}else{if(n._isalpha(e)){return this._process_identifier()}else if(n._isdigit(e)){return this._process_number()}else{throw new SyntaxError("Token error at character "+e+" at position "+this.pos)}}};n._isdigit=function(e){return e>="0"&&e<="9"};n._isalpha=function(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"};n._isalphanum=function(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"||e>="0"&&e<="9"};n.prototype._process_digits=function(e){var t=e;while(t0){throw new TypeError("Invalid Argument ("+e.toString()+"): Divisor must be of type Integer or Fraction.")}else{var t=e.constants[0];return new s(t.numer,t.denom)}};a.prototype.parseFactor=function(){if(this.match("num")){var e=this.parseNumber();this.update();return e}else if(this.match("id")){var t=new i(this.current_token.value);this.update();return t}else if(this.match("lparen")){this.update();var r=this.parseExpr();if(this.match("rparen")){this.update();return r}else{throw new SyntaxError("Unbalanced Parenthesis")}}else{return undefined}};a.prototype.parseNumber=function(){if(parseInt(this.current_token.value)==this.current_token.value){return new i(parseInt(this.current_token.value))}else{var e=this.current_token.value.split(".");var t=e[1].length;var r=Math.pow(10,t);var n=parseFloat(this.current_token.value);return new i(parseInt(n*r)).divide(r)}};t.exports=a},{"./equations":2,"./expressions":3,"./fractions":4,"./lexer":6}]},{},[1])(1)});
;
var H5P = H5P || {};
/**
* Defines the H5P.ArithmeticQuiz class
*/
H5P.ArithmeticQuiz = (function ($) {
/**
* Creates a new ArithmeticQuiz instance
*
* @class
* @augments H5P.EventDispatcher
* @namespace H5P
* @param {Object} options
* @param {number} id
*/
function ArithmeticQuiz(options, id) {
// Add viewport meta to iframe
$('head').append('');
var self = this;
// Extend defaults with provided options
self.options = $.extend(true, {}, {
intro: '',
quizType: 'arithmetic',
arithmeticType: 'addition',
equationType: undefined,
useFractions: undefined,
maxQuestions: undefined,
UI: {
score: 'Score @score',
scoreInPercent: '(@percent% correct)',
time: 'Time: @time',
resultPageHeader: 'Finished!',
retryButton: 'Retry',
startButton: 'Start',
go: 'GO!',
correctText: 'Correct',
incorrectText: 'Incorrect. Correct answer was :num',
durationLabel: 'Duration in hours, minutes and seconds.',
humanizedQuestion: 'What does :arithmetic equal?',
humanizedEquation: 'For the equation :equation, what does :item equal?',
humanizedVariable: 'What does :item equal?',
plusOperator: 'plus',
minusOperator: 'minus',
multiplicationOperator: 'times',
divisionOperator: 'divided by',
equalitySign: 'equal',
slideOfTotal: 'Slide :num of :total'
}
}, options);
self.currentWidth = 0;
self.gamePage = new H5P.ArithmeticQuiz.GamePage(self.options.quizType, self.options, id);
self.gamePage.on('last-slide', function (e) {
self.triggerXAPIScored(e.data.score, e.data.numQuestions, 'answered');
});
self.gamePage.on('started-quiz', function () {
self.setActivityStarted();
});
self.gamePage.on('alternative-chosen', function () {
self.triggerXAPI('interacted');
});
self.introPage = new H5P.ArithmeticQuiz.IntroPage(self.options.intro, self.options.UI);
self.introPage.on('start-game', function() {
self.introPage.remove();
self.gamePage.startCountdown();
});
self.on('resize', function () {
// Set size based on gamePage
var height = self.gamePage.getMaxHeight() + 'px';
this.$container.css({height: height});
// Need to set height in pixels because of FF-bug
$('.h5p-baq-countdown').css({height: height});
$('.h5p-baq-result-page').css({height: height});
});
/**
* Attach function called by H5P framework to insert H5P content into page
*
* @param {H5P.jQuery} $container
*/
self.attach = function ($container) {
if (self.isRoot()) {
self.setActivityStarted();
}
if (this.$container === undefined) {
this.$container = $container;
this.$container.addClass('h5p-baq');
this.introPage.appendTo($container);
// Set gamePage xAPI parameters and append it.
self.gamePage.contentId = id;
self.gamePage.libraryInfo = self.libraryInfo;
self.gamePage.appendTo(self.$container);
self.trigger('resize');
setTimeout(function () {
H5P.ArithmeticQuiz.SoundEffects.setup(self.getLibraryFilePath(''));
}, 1);
}
};
}
/**
* Replaces placeholders in translatables texts
*
* @static
* @param {String} text description
* @param {Object} vars description
* @return {String} description
*/
ArithmeticQuiz.tReplace = function (text, vars) {
for (var placeholder in vars) {
text = text.replace('@'+placeholder, vars[placeholder]);
}
return text;
};
return ArithmeticQuiz;
})(H5P.jQuery);
/**
* Enum defining the different arithmetic types
* @readonly
* @enum {string}
*/
H5P.ArithmeticQuiz.ArithmeticType = {
ADDITION: 'addition',
SUBTRACTION: 'subtraction',
MULTIPLICATION: 'multiplication',
DIVISION: 'division'
};
/**
* Enum defining the different equation types
* @readonly
* @enum {string}
*/
H5P.ArithmeticQuiz.EquationType = {
BASIC: 'basic',
INTERMEDIATE: 'intermediate',
ADVANCED: 'advanced'
};
/**
* Enum defining the different quiz types
* @readonly
* @enum {string}
*/
H5P.ArithmeticQuiz.QuizType = {
ARITHMETIC: 'arithmetic',
LINEAREQUATION: 'linearEquation'
};;
H5P.ArithmeticQuiz.SoundEffects = (function () {
let isDefined = false;
var SoundEffects = {
types: [
'positive-short',
'negative-short'
],
sounds: [],
muted: false
};
const players = {};
/**
* Setup defined sounds
*
* @return {boolean} True if setup was successfull, otherwise false
*/
SoundEffects.setup = function (libraryPath) {
if (isDefined) {
return false;
}
isDefined = true;
SoundEffects.types.forEach(async (type) => {
const player = new Audio();
const extension = player.canPlayType('audio/ogg') ? 'ogg' : 'mp3';
const response = await fetch(libraryPath + 'sounds/' + type + '.' + extension);
const data = await response.blob();
player.src = URL.createObjectURL(data);
players[type] = player;
});
return true;
};
/**
* Play a sound
*
* @param {string} type Name of the sound as defined in [SoundEffects.types]
* @param {number} delay Delay in milliseconds
*/
SoundEffects.play = function (type, delay) {
if (SoundEffects.muted === false) {
if (!players[type]) {
return;
}
setTimeout(function () {
players[type].play();
}, delay || 0);
}
};
/**
* Mute. Subsequent invocations of SoundEffects.play() will not make any sounds beeing played.
*/
SoundEffects.mute = function () {
SoundEffects.muted = true;
};
/**
* Unmute
*/
SoundEffects.unmute = function () {
SoundEffects.muted = false;
};
return SoundEffects;
})();
;
/**
* Defines the H5P.ArithmeticQuiz.CountdownWidget class
*/
H5P.ArithmeticQuiz.CountdownWidget = (function ($) {
/**
* A count down widget
*
* @class
* @augments H5P.EventDispatcher
* @namespace H5P.ArithmeticQuiz
* @fires H5P.Event
*
* @param {number} seconds Number of seconds to count down
* @param {Object} t Translations
*/
function CountdownWidget(seconds, t) {
H5P.EventDispatcher.call(this);
var originalSeconds = seconds;
this.$countdownWidget = $('