Actualización

This commit is contained in:
Xes
2025-04-10 12:36:07 +02:00
parent 1da7c3f3b9
commit 4aff98e77b
3147 changed files with 320647 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
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 = $('<div>', {
'class': 'joubel-help-text-dialog-box',
'role': 'dialog',
'aria-labelledby': headerId,
'aria-describedby': helpTextId
});
$('<div>', {
'class': 'joubel-help-text-dialog-background'
}).appendTo($helpTextDialogBox);
var $helpTextDialogContainer = $('<div>', {
'class': 'joubel-help-text-dialog-container'
}).appendTo($helpTextDialogBox);
$('<div>', {
'class': 'joubel-help-text-header',
'id': headerId,
'role': 'header',
'html': header
}).appendTo($helpTextDialogContainer);
$('<div>', {
'class': 'joubel-help-text-body',
'id': helpTextId,
'html': message,
'role': 'document',
'tabindex': 0
}).appendTo($helpTextDialogContainer);
var handleClose = function () {
$helpTextDialogBox.remove();
self.trigger('closed');
};
var $closeButton = $('<div>', {
'class': 'joubel-help-text-remove',
'role': 'button',
'title': closeButtonTitle,
'tabindex': 1,
'click': handleClose,
'keydown': function (event) {
// 32 - space, 13 - enter
if ([32, 13].indexOf(event.which) !== -1) {
event.preventDefault();
handleClose();
}
}
}).appendTo($helpTextDialogContainer);
/**
* Get the DOM element
* @return {HTMLElement}
*/
self.getElement = function () {
return $helpTextDialogBox;
};
self.focus = function () {
$closeButton.focus();
};
}
JoubelHelpTextDialog.prototype = Object.create(H5P.EventDispatcher.prototype);
JoubelHelpTextDialog.prototype.constructor = JoubelHelpTextDialog;
return JoubelHelpTextDialog;
}(H5P.jQuery));

View File

@@ -0,0 +1,38 @@
var H5P = H5P || {};
/**
* Class responsible for creating auto-disappearing dialogs
*/
H5P.JoubelMessageDialog = (function ($) {
/**
* 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
* @return {H5P.jQuery}
*/
function JoubelMessageDialog ($container, message) {
var timeout;
var removeDialog = function () {
$warning.remove();
clearTimeout(timeout);
$container.off('click.messageDialog');
};
// Create warning popup:
var $warning = $('<div/>', {
'class': 'joubel-message-dialog',
text: message
}).appendTo($container);
// Remove after 3 seconds or if user clicks anywhere in $container:
timeout = setTimeout(removeDialog, 3000);
$container.on('click.messageDialog', removeDialog);
return $warning;
}
return JoubelMessageDialog;
})(H5P.jQuery);

View File

@@ -0,0 +1,159 @@
var H5P = H5P || {};
/**
* Class responsible for creating a circular progress bar
*/
H5P.JoubelProgressCircle = (function ($) {
/**
* Constructor for the Progress Circle
*
* @param {Number} number The amount of progress to display
* @param {string} progressColor Color for the progress meter
* @param {string} backgroundColor Color behind the progress meter
*/
function ProgressCircle(number, progressColor, fillColor, backgroundColor) {
progressColor = progressColor || '#1a73d9';
fillColor = fillColor || '#f0f0f0';
backgroundColor = backgroundColor || '#ffffff';
var progressColorRGB = this.hexToRgb(progressColor);
//Verify number
try {
number = Number(number);
if (number === '') {
throw 'is empty';
}
if (isNaN(number)) {
throw 'is not a number';
}
} catch (e) {
number = 'err';
}
//Draw circle
if (number > 100) {
number = 100;
}
// We can not use rgba, since they will stack on top of each other.
// Instead we create the equivalent of the rgba color
// and applies this to the activeborder and background color.
var progressColorString = 'rgb(' + parseInt(progressColorRGB.r, 10) +
',' + parseInt(progressColorRGB.g, 10) +
',' + parseInt(progressColorRGB.b, 10) + ')';
// Circle wrapper
var $wrapper = $('<div/>', {
'class': "joubel-progress-circle-wrapper"
});
//Active border indicates progress
var $activeBorder = $('<div/>', {
'class': "joubel-progress-circle-active-border"
}).appendTo($wrapper);
//Background circle
var $backgroundCircle = $('<div/>', {
'class': "joubel-progress-circle-circle"
}).appendTo($activeBorder);
//Progress text/number
$('<span/>', {
'text': number + '%',
'class': "joubel-progress-circle-percentage"
}).appendTo($backgroundCircle);
var deg = number * 3.6;
if (deg <= 180) {
$activeBorder.css('background-image',
'linear-gradient(' + (90 + deg) + 'deg, transparent 50%, ' + fillColor + ' 50%),' +
'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)')
.css('border', '2px solid' + backgroundColor)
.css('background-color', progressColorString);
} else {
$activeBorder.css('background-image',
'linear-gradient(' + (deg - 90) + 'deg, transparent 50%, ' + progressColorString + ' 50%),' +
'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)')
.css('border', '2px solid' + backgroundColor)
.css('background-color', progressColorString);
}
this.$activeBorder = $activeBorder;
this.$backgroundCircle = $backgroundCircle;
this.$wrapper = $wrapper;
this.initResizeFunctionality();
return $wrapper;
}
/**
* Initializes resize functionality for the progress circle
*/
ProgressCircle.prototype.initResizeFunctionality = function () {
var self = this;
$(window).resize(function () {
// Queue resize
setTimeout(function () {
self.resize();
});
});
// First resize
setTimeout(function () {
self.resize();
}, 0);
};
/**
* Resize function makes progress circle grow or shrink relative to parent container
*/
ProgressCircle.prototype.resize = function () {
var $parent = this.$wrapper.parent();
if ($parent !== undefined && $parent) {
// Measurements
var fontSize = parseInt($parent.css('font-size'), 10);
// Static sizes
var fontSizeMultiplum = 3.75;
var progressCircleWidthPx = parseInt((fontSize / 4.5), 10) % 2 === 0 ? parseInt((fontSize / 4.5), 10) + 4 : parseInt((fontSize / 4.5), 10) + 5;
var progressCircleOffset = progressCircleWidthPx / 2;
var width = fontSize * fontSizeMultiplum;
var height = fontSize * fontSizeMultiplum;
this.$activeBorder.css({
'width': width,
'height': height
});
this.$backgroundCircle.css({
'width': width - progressCircleWidthPx,
'height': height - progressCircleWidthPx,
'top': progressCircleOffset,
'left': progressCircleOffset
});
}
};
/**
* Hex to RGB conversion
* @param hex
* @returns {{r: Number, g: Number, b: Number}}
*/
ProgressCircle.prototype.hexToRgb = function (hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
return ProgressCircle;
}(H5P.jQuery));

View File

@@ -0,0 +1,190 @@
var H5P = H5P || {};
H5P.JoubelProgressbar = (function ($) {
/**
* Joubel progressbar class
* @method JoubelProgressbar
* @constructor
* @param {number} steps Number 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"
*/
function JoubelProgressbar(steps, options) {
H5P.EventDispatcher.call(this);
var self = this;
this.options = $.extend({
progressText: 'Slide :num of :total'
}, options);
this.currentStep = 0;
this.steps = steps;
this.$progressbar = $('<div>', {
'class': 'h5p-joubelui-progressbar',
on: {
click: function () {
self.toggleTooltip();
return false;
},
mouseenter: function () {
self.showTooltip();
},
mouseleave: function () {
setTimeout(function () {
self.hideTooltip();
}, 1500);
}
}
});
this.$background = $('<div>', {
'class': 'h5p-joubelui-progressbar-background'
}).appendTo(this.$progressbar);
$('body').click(function () {
self.toggleTooltip(true);
});
}
JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype);
JoubelProgressbar.prototype.constructor = JoubelProgressbar;
/**
* Display tooltip
* @method showTooltip
*/
JoubelProgressbar.prototype.showTooltip = function () {
var self = this;
if (this.currentStep === 0 || this.tooltip !== undefined) {
return;
}
var parentWidth = self.$progressbar.offset().left + self.$progressbar.width();
this.tooltip = new H5P.Drop({
target: this.$background.get(0),
content: this.currentStep + '/' + this.steps,
classes: 'drop-theme-arrows-bounce h5p-joubelui-drop',
position: 'top right',
openOn: 'always',
tetherOptions: {
attachment: 'bottom center',
targetAttachment: 'top right'
}
});
this.tooltip.on('open', function () {
var $drop = $(self.tooltip.drop);
var left = $drop.position().left;
var dropWidth = $drop.width();
// Need to handle drops getting outside of the progressbar:
if (left < 0) {
$drop.css({marginLeft: (-left) + 'px'});
}
else if (left + dropWidth > parentWidth) {
$drop.css({marginLeft: (parentWidth - (left + dropWidth)) + 'px'});
}
});
};
JoubelProgressbar.prototype.updateAria = function () {
var self = this;
if (this.options.disableAria) {
return;
}
if (!this.$currentStatus) {
this.$currentStatus = $('<div>', {
'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);
};
/**
* Hides tooltip
* @method hideTooltip
*/
JoubelProgressbar.prototype.hideTooltip = function () {
if (this.tooltip !== undefined) {
this.tooltip.remove();
this.tooltip.destroy();
this.tooltip = undefined;
}
};
/**
* Toggles tooltip-visibility
* @method toggleTooltip
* @param {boolean} [closeOnly] Don't show, only close if open
*/
JoubelProgressbar.prototype.toggleTooltip = function (closeOnly) {
if (this.tooltip === undefined && !closeOnly) {
this.showTooltip();
}
else if (this.tooltip !== undefined) {
this.hideTooltip();
}
};
/**
* 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);

View File

@@ -0,0 +1,225 @@
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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 63.77 53.87" aria-hidden="true" focusable="false">' +
'<title>star</title>' +
'<filter id="h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + '" x0="-50%" y0="-50%" width="200%" height="200%">' +
'<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"></feGaussianBlur>' +
'<feOffset dy="2" dx="4"></feOffset>' +
'<feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' +
'<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' +
'<feComposite in2="shadowDiff" operator="in"></feComposite>' +
'<feComposite in2="SourceGraphic" operator="over" result="firstfilter"></feComposite>' +
'<feGaussianBlur in="firstfilter" stdDeviation="3" result="blur2"></feGaussianBlur>' +
'<feOffset dy="-2" dx="-4"></feOffset>' +
'<feComposite in2="firstfilter" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' +
'<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' +
'<feComposite in2="shadowDiff" operator="in"></feComposite>' +
'<feComposite in2="firstfilter" operator="over"></feComposite>' +
'</filter>' +
'<path class="h5p-joubelui-score-bar-star-shadow" d="M35.08,43.41V9.16H20.91v0L9.51,10.85,9,10.93C2.8,12.18,0,17,0,21.25a11.22,11.22,0,0,0,3,7.48l8.73,8.53-1.07,6.16Z"/>' +
'<g>' +
'<path class="h5p-joubelui-score-bar-star-border" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
'<path class="h5p-joubelui-score-bar-star-fill" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
'<path filter="url(#h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + ')" class="h5p-joubelui-score-bar-star-fill-full-score" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
'</g>' +
'</svg>';
/**
* @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 = $('<div>', {
'class': 'h5p-joubelui-score-bar',
});
var $visuals = $('<div>', {
'class': 'h5p-joubelui-score-bar-visuals',
appendTo: self.$scoreBar
});
// The progress bar wrapper
self.$progressWrapper = $('<div>', {
'class': 'h5p-joubelui-score-bar-progress-wrapper',
appendTo: $visuals
});
self.$progress = $('<div>', {
'class': 'h5p-joubelui-score-bar-progress',
'html': createLabel(self.score),
appendTo: self.$progressWrapper
});
// The star
$('<div>', {
'class': 'h5p-joubelui-score-bar-star',
html: self.STAR_MARKUP
}).appendTo($visuals);
// The score container
var $numerics = $('<div>', {
'class': 'h5p-joubelui-score-numeric',
appendTo: self.$scoreBar,
'aria-hidden': true
});
// The current score
self.$scoreCounter = $('<span>', {
'class': 'h5p-joubelui-score-number h5p-joubelui-score-number-counter',
text: 0,
appendTo: $numerics
});
// The separator
$('<span>', {
'class': 'h5p-joubelui-score-number-separator',
text: '/',
appendTo: $numerics
});
// Max score
self.$maxScore = $('<span>', {
'class': 'h5p-joubelui-score-number h5p-joubelui-score-max',
text: self.maxScore,
appendTo: $numerics
});
if (helpText) {
H5P.JoubelUI.createTip(helpText, {
tipLabel: scoreExplanationButtonLabel ? scoreExplanationButtonLabel : helpText,
helpIcon: true
}).appendTo(self.$scoreBar);
self.$scoreBar.addClass('h5p-score-bar-has-help');
}
};
/**
* Set the current score
* @method setScore
* @memberOf H5P.JoubelScoreBar#
* @param {number} score
*/
self.setScore = function (score) {
// Do nothing if score hasn't changed
if (score === self.score) {
return;
}
self.score = score > self.maxScore ? self.maxScore : score;
self.updateVisuals();
};
/**
* Increment score
* @method incrementScore
* @memberOf H5P.JoubelScoreBar#
* @param {number=} incrementBy Optional parameter, defaults to 1
*/
self.incrementScore = function (incrementBy) {
self.setScore(self.score + (incrementBy || 1));
};
/**
* Set the max score
* @method setMaxScore
* @memberOf H5P.JoubelScoreBar#
* @param {number} maxScore The max score
*/
self.setMaxScore = function (maxScore) {
self.maxScore = maxScore;
};
/**
* Updates the progressbar visuals
* @memberOf H5P.JoubelScoreBar#
* @method updateVisuals
*/
self.updateVisuals = function () {
self.$progress.html(createLabel(self.score));
self.$scoreCounter.text(self.score);
self.$maxScore.text(self.maxScore);
setTimeout(function () {
// Start the progressbar animation
self.$progress.css({
width: ((self.score / self.maxScore) * 100) + '%'
});
H5P.Transition.onTransitionEnd(self.$progress, function () {
// If fullscore fill the star and start the animation
self.$scoreBar.toggleClass('h5p-joubelui-score-bar-full-score', self.score === self.maxScore);
self.$scoreBar.toggleClass('h5p-joubelui-score-bar-animation-active', self.score === self.maxScore);
// Only allow the star animation to run once
self.$scoreBar.one("animationend", function() {
self.$scoreBar.removeClass("h5p-joubelui-score-bar-animation-active");
});
}, 600);
}, 300);
};
/**
* Removes all classes
* @method reset
*/
self.reset = function () {
self.$scoreBar.removeClass('h5p-joubelui-score-bar-full-score');
};
createHtml();
}
return JoubelScoreBar;
})(H5P.jQuery);

View File

@@ -0,0 +1,32 @@
var H5P = H5P || {};
H5P.SimpleRoundedButton = (function ($) {
/**
* Creates a new tip
*/
function SimpleRoundedButton(text) {
var $simpleRoundedButton = $('<div>', {
'class': 'joubel-simple-rounded-button',
'title': text,
'role': 'button',
'tabindex': '0'
}).keydown(function (e) {
// 32 - space, 13 - enter
if ([32, 13].indexOf(e.which) !== -1) {
$(this).click();
e.preventDefault();
}
});
$('<span>', {
'class': 'joubel-simple-rounded-button-text',
'html': text
}).appendTo($simpleRoundedButton);
return $simpleRoundedButton;
}
return SimpleRoundedButton;
}(H5P.jQuery));

View File

@@ -0,0 +1,96 @@
var H5P = H5P || {};
H5P.JoubelSlider = (function ($) {
/**
* Creates a new Slider
*
* @param {object} [params] Additional parameters
*/
function JoubelSlider(params) {
H5P.EventDispatcher.call(this);
this.$slider = $('<div>', $.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);

View File

@@ -0,0 +1,356 @@
var H5P = H5P || {};
/**
* Class responsible for creating speech bubbles
*/
H5P.JoubelSpeechBubble = (function ($) {
var $currentSpeechBubble;
var $currentContainer;
var $tail;
var $innerTail;
var removeSpeechBubbleTimeout;
var currentMaxWidth;
var DEFAULT_MAX_WIDTH = 400;
var iDevice = navigator.userAgent.match(/iPod|iPhone|iPad/g) ? true : false;
/**
* Creates a new speech bubble
*
* @param {H5P.jQuery} $container The speaking object
* @param {string} text The text to display
* @param {number} maxWidth The maximum width of the bubble
* @return {H5P.JoubelSpeechBubble}
*/
function JoubelSpeechBubble($container, text, maxWidth) {
maxWidth = maxWidth || DEFAULT_MAX_WIDTH;
currentMaxWidth = maxWidth;
$currentContainer = $container;
this.isCurrent = function ($tip) {
return $tip.is($currentContainer);
};
this.remove = function () {
remove();
};
var fadeOutSpeechBubble = function ($speechBubble) {
if (!$speechBubble) {
return;
}
// Stop removing bubble
clearTimeout(removeSpeechBubbleTimeout);
$speechBubble.removeClass('show');
setTimeout(function () {
if ($speechBubble) {
$speechBubble.remove();
$speechBubble = undefined;
}
}, 500);
};
if ($currentSpeechBubble !== undefined) {
remove();
}
var $h5pContainer = getH5PContainer($container);
// Make sure we fade out old speech bubble
fadeOutSpeechBubble($currentSpeechBubble);
// Create bubble
$tail = $('<div class="joubel-speech-bubble-tail"></div>');
$innerTail = $('<div class="joubel-speech-bubble-inner-tail"></div>');
var $innerBubble = $(
'<div class="joubel-speech-bubble-inner">' +
'<div class="joubel-speech-bubble-text">' + text + '</div>' +
'</div>'
).prepend($innerTail);
$currentSpeechBubble = $(
'<div class="joubel-speech-bubble" aria-live="assertive">'
).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);

View File

@@ -0,0 +1,19 @@
var H5P = H5P || {};
H5P.JoubelThrobber = (function ($) {
/**
* Creates a new tip
*/
function JoubelThrobber() {
// h5p-throbber css is described in core
var $throbber = $('<div/>', {
'class': 'h5p-throbber'
});
return $throbber;
}
return JoubelThrobber;
}(H5P.jQuery));

View File

@@ -0,0 +1,106 @@
H5P.JoubelTip = (function ($) {
var $conv = $('<div/>');
/**
* 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 = $('<div/>', {
class: 'joubel-tip-container' + (behaviour.showSpeechBubble ? '' : ' be-quiet'),
title: behaviour.tipLabel,
'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: '<span class="joubel-icon-tip-normal ' + (behaviour.helpIcon ? ' help-icon': '') + '">' +
'<span class="h5p-icon-shadow"></span>' +
'<span class="h5p-icon-speech-bubble"></span>' +
'<span class="h5p-icon-info"></span>' +
'</span>'
// IMPORTANT: All of the markup elements must have 'pointer-events: none;'
});
const $tipAnnouncer = $('<div>', {
'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);

View File

@@ -0,0 +1,183 @@
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);