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 = $('
', { 'class': 'joubel-help-text-dialog-box', 'role': 'dialog', 'aria-labelledby': headerId, 'aria-describedby': helpTextId }); $('
', { 'class': 'joubel-help-text-dialog-background' }).appendTo($helpTextDialogBox); var $helpTextDialogContainer = $('
', { 'class': 'joubel-help-text-dialog-container' }).appendTo($helpTextDialogBox); $('
', { 'class': 'joubel-help-text-header', 'id': headerId, 'role': 'header', 'html': header }).appendTo($helpTextDialogContainer); $('
', { '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 = $('
', { '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)); ; 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 = $('
', { '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); ; 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 = $('
', { 'class': "joubel-progress-circle-wrapper" }); //Active border indicates progress var $activeBorder = $('
', { 'class': "joubel-progress-circle-active-border" }).appendTo($wrapper); //Background circle var $backgroundCircle = $('
', { 'class': "joubel-progress-circle-circle" }).appendTo($activeBorder); //Progress text/number $('', { '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)); ; var H5P = H5P || {}; H5P.SimpleRoundedButton = (function ($) { /** * Creates a new tip */ function SimpleRoundedButton(text) { var $simpleRoundedButton = $('
', { '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(); } }); $('', { 'class': 'joubel-simple-rounded-button-text', 'html': text }).appendTo($simpleRoundedButton); return $simpleRoundedButton; } return SimpleRoundedButton; }(H5P.jQuery)); ; 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 = $('
'); $innerTail = $('
'); var $innerBubble = $( '
' + '
' + text + '
' + '
' ).prepend($innerTail); $currentSpeechBubble = $( '
' ).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-score-bar-progress-wrapper', appendTo: $visuals }); self.$progress = $('
', { 'class': 'h5p-joubelui-score-bar-progress', 'html': createLabel(self.score), appendTo: self.$progressWrapper }); // The star $('
', { 'class': 'h5p-joubelui-score-bar-star', html: self.STAR_MARKUP }).appendTo($visuals); // The score container var $numerics = $('
', { 'class': 'h5p-joubelui-score-numeric', appendTo: self.$scoreBar, 'aria-hidden': true }); // The current score self.$scoreCounter = $('', { 'class': 'h5p-joubelui-score-number h5p-joubelui-score-number-counter', text: 0, appendTo: $numerics }); // The separator $('', { 'class': 'h5p-joubelui-score-number-separator', text: '/', appendTo: $numerics }); // Max score self.$maxScore = $('', { '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); ; 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 = $('
', { 'class': 'h5p-joubelui-progressbar' }); this.$background = $('
', { '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); ; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var H5P = H5P || {}; /** * H5P-Timer * * General purpose timer that can be used by other H5P libraries. * * @param {H5P.jQuery} $ */ H5P.Timer = function ($, EventDispatcher) { /** * Create a timer. * * @constructor * @param {number} [interval=Timer.DEFAULT_INTERVAL] - The update interval. */ function Timer() { var interval = arguments.length <= 0 || arguments[0] === undefined ? Timer.DEFAULT_INTERVAL : arguments[0]; var self = this; // time on clock and the time the clock has run var clockTimeMilliSeconds = 0; var playingTimeMilliSeconds = 0; // used to update recurring notifications var clockUpdateMilliSeconds = 0; // indicators for total running time of the timer var firstDate = null; var startDate = null; var lastDate = null; // update loop var loop = null; // timer status var status = Timer.STOPPED; // indicate counting direction var mode = Timer.FORWARD; // notifications var notifications = []; // counter for notifications; var notificationsIdCounter = 0; // Inheritance H5P.EventDispatcher.call(self); // sanitize interval if (Timer.isInteger(interval)) { interval = Math.max(interval, 1); } else { interval = Timer.DEFAULT_INTERVAL; } /** * Get the timer status. * * @public * @return {number} The timer status. */ self.getStatus = function () { return status; }; /** * Get the timer mode. * * @public * @return {number} The timer mode. */ self.getMode = function () { return mode; }; /** * Get the time that's on the clock. * * @private * @return {number} The time on the clock. */ var getClockTime = function getClockTime() { return clockTimeMilliSeconds; }; /** * Get the time the timer was playing so far. * * @private * @return {number} The time played. */ var getPlayingTime = function getPlayingTime() { return playingTimeMilliSeconds; }; /** * Get the total running time from play() until stop(). * * @private * @return {number} The total running time. */ var getRunningTime = function getRunningTime() { if (!firstDate) { return 0; } if (status !== Timer.STOPPED) { return new Date().getTime() - firstDate.getTime(); } else { return !lastDate ? 0 : lastDate.getTime() - firstDate; } }; /** * Get one of the times. * * @public * @param {number} [type=Timer.TYPE_CLOCK] - Type of the time to get. * @return {number} Clock Time, Playing Time or Running Time. */ self.getTime = function () { var type = arguments.length <= 0 || arguments[0] === undefined ? Timer.TYPE_CLOCK : arguments[0]; if (!Timer.isInteger(type)) { return; } // break will never be reached, but for consistency... switch (type) { case Timer.TYPE_CLOCK: return getClockTime(); break; case Timer.TYPE_PLAYING: return getPlayingTime(); break; case Timer.TYPE_RUNNING: return getRunningTime(); break; default: return getClockTime(); } }; /** * Set the clock time. * * @public * @param {number} time - The time in milliseconds. */ self.setClockTime = function (time) { if ($.type(time) === 'string') { time = Timer.toMilliseconds(time); } if (!Timer.isInteger(time)) { return; } // notifications only need an update if changing clock against direction clockUpdateMilliSeconds = (time - clockTimeMilliSeconds) * mode < 0 ? time - clockTimeMilliSeconds : 0; clockTimeMilliSeconds = time; }; /** * Reset the timer. * * @public */ self.reset = function () { if (status !== Timer.STOPPED) { return; } clockTimeMilliSeconds = 0; playingTimeMilliSeconds = 0; firstDate = null; lastDate = null; loop = null; notifications = []; notificationsIdCounter = 0; self.trigger('reset', {}, {bubbles: true, external: true}); }; /** * Set timer mode. * * @public * @param {number} mode - The timer mode. */ self.setMode = function (direction) { if (direction !== Timer.FORWARD && direction !== Timer.BACKWARD) { return; } mode = direction; }; /** * Start the timer. * * @public */ self.play = function () { if (status === Timer.PLAYING) { return; } if (!firstDate) { firstDate = new Date(); } startDate = new Date(); status = Timer.PLAYING; self.trigger('play', {}, {bubbles: true, external: true}); update(); }; /** * Pause the timer. * * @public */ self.pause = function () { if (status !== Timer.PLAYING) { return; } status = Timer.PAUSED; self.trigger('pause', {}, {bubbles: true, external: true}); }; /** * Stop the timer. * * @public */ self.stop = function () { if (status === Timer.STOPPED) { return; } lastDate = new Date(); status = Timer.STOPPED; self.trigger('stop', {}, {bubbles: true, external: true}); }; /** * Update the timer until Timer.STOPPED. * * @private */ var update = function update() { var currentMilliSeconds = 0; // stop because requested if (status === Timer.STOPPED) { clearTimeout(loop); return; } //stop because countdown reaches 0 if (mode === Timer.BACKWARD && clockTimeMilliSeconds <= 0) { self.stop(); return; } // update times if (status === Timer.PLAYING) { currentMilliSeconds = new Date().getTime() - startDate; clockTimeMilliSeconds += currentMilliSeconds * mode; playingTimeMilliSeconds += currentMilliSeconds; } startDate = new Date(); checkNotifications(); loop = setTimeout(function () { update(); }, interval); }; /** * Get next notification id. * * @private * @return {number} id - The next id. */ var getNextNotificationId = function getNextNotificationId() { return notificationsIdCounter++; }; /** * Set a notification * * @public * @param {Object|String} params - Parameters for the notification. * @callback callback - Callback function. * @return {number} ID of the notification. */ self.notify = function (params, callback) { var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getNextNotificationId(); // common default values for the clock timer // TODO: find a better place for this, maybe a JSON file? var defaults = {}; defaults['every_tenth_second'] = { "repeat": 100 }; defaults['every_second'] = { "repeat": 1000 }; defaults['every_minute'] = { "repeat": 60000 }; defaults['every_hour'] = { "repeat": 3600000 }; // Sanity check for callback function if (!callback instanceof Function) { return; } // Get default values if ($.type(params) === 'string') { params = defaults[params]; } if (params !== null && (typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') { // Sanitize type if (!params.type) { params.type = Timer.TYPE_CLOCK; } else { if (!Timer.isInteger(params.type)) { return; } if (params.type < Timer.TYPE_CLOCK || params.type > Timer.TYPE_RUNNING) { return; } } // Sanitize mode if (!params.mode) { params.mode = Timer.NOTIFY_ABSOLUTE; } else { if (!Timer.isInteger(params.mode)) { return; } if (params.mode < Timer.NOTIFY_ABSOLUTE || params.type > Timer.NOTIFY_RELATIVE) { return; } } // Sanitize calltime if (!params.calltime) { params.calltime = params.mode === Timer.NOTIFY_ABSOLUTE ? self.getTime(params.type) : 0; } else { if ($.type(params.calltime) === 'string') { params.calltime = Timer.toMilliseconds(params.calltime); } if (!Timer.isInteger(params.calltime)) { return; } if (params.calltime < 0) { return; } if (params.mode === Timer.NOTIFY_RELATIVE) { params.calltime = Math.max(params.calltime, interval); if (params.type === Timer.TYPE_CLOCK) { // clock could be running backwards params.calltime *= mode; } params.calltime += self.getTime(params.type); } } // Sanitize repeat if ($.type(params.repeat) === 'string') { params.repeat = Timer.toMilliseconds(params.repeat); } // repeat must be >= interval (ideally multiple of interval) if (params.repeat !== undefined) { if (!Timer.isInteger(params.repeat)) { return; } params.repeat = Math.max(params.repeat, interval); } } else { // neither object nor string return; } // add notification notifications.push({ 'id': id, 'type': params.type, 'calltime': params.calltime, 'repeat': params.repeat, 'callback': callback }); return id; }; /** * Remove a notification. * * @public * @param {number} id - The id of the notification. */ self.clearNotification = function (id) { notifications = $.grep(notifications, function (item) { return item.id === id; }, true); }; /** * Set a new starting time for notifications. * * @private * @param elements {Object] elements - The notifications to be updated. * @param deltaMilliSeconds {Number} - The time difference to be set. */ var updateNotificationTime = function updateNotificationTime(elements, deltaMilliSeconds) { if (!Timer.isInteger(deltaMilliSeconds)) { return; } elements.forEach(function (element) { // remove notification self.clearNotification(element.id); //rebuild notification with new data self.notify({ 'type': element.type, 'calltime': self.getTime(element.type) + deltaMilliSeconds, 'repeat': element.repeat }, element.callback, element.id); }); }; /** * Check notifications for necessary callbacks. * * @private */ var checkNotifications = function checkNotifications() { var backwards = 1; var elements = []; // update recurring clock notifications if clock was changed if (clockUpdateMilliSeconds !== 0) { elements = $.grep(notifications, function (item) { return item.type === Timer.TYPE_CLOCK && item.repeat != undefined; }); updateNotificationTime(elements, clockUpdateMilliSeconds); clockUpdateMilliSeconds = 0; } // check all notifications for triggering notifications.forEach(function (element) { /* * trigger if notification time is in the past * which means calltime >= Clock Time if mode is BACKWARD (= -1) */ backwards = element.type === Timer.TYPE_CLOCK ? mode : 1; if (element.calltime * backwards <= self.getTime(element.type) * backwards) { // notify callback function element.callback.apply(this); // remove notification self.clearNotification(element.id); // You could use updateNotificationTime() here, but waste some time // rebuild notification if it should be repeated if (element.repeat) { self.notify({ 'type': element.type, 'calltime': self.getTime(element.type) + element.repeat * backwards, 'repeat': element.repeat }, element.callback, element.id); } } }); }; } // Inheritance Timer.prototype = Object.create(H5P.EventDispatcher.prototype); Timer.prototype.constructor = Timer; /** * Generate timecode elements from milliseconds. * * @private * @param {number} milliSeconds - The milliseconds. * @return {Object} The timecode elements. */ var toTimecodeElements = function toTimecodeElements(milliSeconds) { var years = 0; var month = 0; var weeks = 0; var days = 0; var hours = 0; var minutes = 0; var seconds = 0; var tenthSeconds = 0; if (!Timer.isInteger(milliSeconds)) { return; } milliSeconds = Math.round(milliSeconds / 100); tenthSeconds = milliSeconds - Math.floor(milliSeconds / 10) * 10; seconds = Math.floor(milliSeconds / 10); minutes = Math.floor(seconds / 60); hours = Math.floor(minutes / 60); days = Math.floor(hours / 24); weeks = Math.floor(days / 7); month = Math.floor(days / 30.4375); // roughly (30.4375 = mean of 4 years) years = Math.floor(days / 365); // roughly (no leap years considered) return { years: years, month: month, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds, tenthSeconds: tenthSeconds }; }; /** * Extract humanized time element from time for concatenating. * * @public * @param {number} milliSeconds - The milliSeconds. * @param {string} element - Time element: hours, minutes, seconds or tenthSeconds. * @param {boolean} [rounded=false] - If true, element value will be rounded. * @return {number} The time element. */ Timer.extractTimeElement = function (time, element) { var rounded = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2]; var timeElements = null; if ($.type(time) === 'string') { time = Timer.toMilliseconds(time); } if (!Timer.isInteger(time)) { return; } if ($.type(element) !== 'string') { return; } if ($.type(rounded) !== 'boolean') { return; } if (rounded) { timeElements = { years: Math.round(time / 31536000000), month: Math.round(time / 2629800000), weeks: Math.round(time / 604800000), days: Math.round(time / 86400000), hours: Math.round(time / 3600000), minutes: Math.round(time / 60000), seconds: Math.round(time / 1000), tenthSeconds: Math.round(time / 100) }; } else { timeElements = toTimecodeElements(time); } return timeElements[element]; }; /** * Convert time in milliseconds to timecode. * * @public * @param {number} milliSeconds - The time in milliSeconds. * @return {string} The humanized timecode. */ Timer.toTimecode = function (milliSeconds) { var timecodeElements = null; var timecode = ''; var minutes = 0; var seconds = 0; if (!Timer.isInteger(milliSeconds)) { return; } if (milliSeconds < 0) { return; } timecodeElements = toTimecodeElements(milliSeconds); minutes = Math.floor(timecodeElements['minutes'] % 60); seconds = Math.floor(timecodeElements['seconds'] % 60); // create timecode if (timecodeElements['hours'] > 0) { timecode += timecodeElements['hours'] + ':'; } if (minutes < 10) { timecode += '0'; } timecode += minutes + ':'; if (seconds < 10) { timecode += '0'; } timecode += seconds + '.'; timecode += timecodeElements['tenthSeconds']; return timecode; }; /** * Convert timecode to milliseconds. * * @public * @param {string} timecode - The timecode. * @return {number} Milliseconds derived from timecode */ Timer.toMilliseconds = function (timecode) { var head = []; var tail = ''; var hours = 0; var minutes = 0; var seconds = 0; var tenthSeconds = 0; if (!Timer.isTimecode(timecode)) { return; } // thx to the regexp we know everything can be converted to a legit integer in range head = timecode.split('.')[0].split(':'); while (head.length < 3) { head = ['0'].concat(head); } hours = parseInt(head[0]); minutes = parseInt(head[1]); seconds = parseInt(head[2]); tail = timecode.split('.')[1]; if (tail) { tenthSeconds = Math.round(parseInt(tail) / Math.pow(10, tail.length - 1)); } return (hours * 36000 + minutes * 600 + seconds * 10 + tenthSeconds) * 100; }; /** * Check if a string is a timecode. * * @public * @param {string} value - String to check * @return {boolean} true, if string is a timecode */ Timer.isTimecode = function (value) { var reg_timecode = /((((((\d+:)?([0-5]))?\d:)?([0-5]))?\d)(\.\d+)?)/; if ($.type(value) !== 'string') { return false; } return value === value.match(reg_timecode)[0] ? true : false; }; // Workaround for IE and potentially other browsers within Timer object Timer.isInteger = Timer.isInteger || function(value) { return typeof value === "number" && isFinite(value) && Math.floor(value) === value; }; // Timer states /** @constant {number} */ Timer.STOPPED = 0; /** @constant {number} */ Timer.PLAYING = 1; /** @constant {number} */ Timer.PAUSED = 2; // Timer directions /** @constant {number} */ Timer.FORWARD = 1; /** @constant {number} */ Timer.BACKWARD = -1; /** @constant {number} */ Timer.DEFAULT_INTERVAL = 10; // Counter types /** @constant {number} */ Timer.TYPE_CLOCK = 0; /** @constant {number} */ Timer.TYPE_PLAYING = 1; /** @constant {number} */ Timer.TYPE_RUNNING = 2; // Notification types /** @constant {number} */ Timer.NOTIFY_ABSOLUTE = 0; /** @constant {number} */ Timer.NOTIFY_RELATIVE = 1; return Timer; }(H5P.jQuery, H5P.EventDispatcher); ; H5P.FindTheWords = (function ($, UI) { const ELEMENT_MIN_SIZE = 32; // PX const ELEMENT_MAX_SIZE = 64; // PX const MARGIN = 8; //PX const VOCABULARY_INLINE_WIDTH = 200;// PX /** * FindTheWords. * @class H5P.FindTheWords * @extends H5P.EventDispatcher * @param {Object} options * @param {number} id * @param {Object} extras */ function FindTheWords(options, id, extras) { /** @alias H5P.FindTheWords# */ this.id = id; this.extras = extras; this.numFound = 0; this.isAttempted = false; this.isGameStarted = false; // Only take the unique words const vocabulary = options.wordList .split(',') .map(function (word) { return word.trim(); }) .filter(function (word, pos, self) { return self.indexOf(word) === pos; }); this.options = $.extend(true, { vocabulary: vocabulary, height: 5, width: 5, fillBlanks: true, maxAttempts: 5, l10n: { wordListHeader: 'Find the words' } }, options); H5P.EventDispatcher.call(this); this.gridParams = { height: this.options.height, width: this.options.width, orientations: filterOrientations(options.behaviour.orientations), fillBlanks: this.options.fillBlanks, maxAttempts: this.options.maxAttempts, preferOverlap: options.behaviour.preferOverlap, vocabulary: this.options.vocabulary, gridActive: true, fillPool: this.options.behaviour.fillPool }; this.grid = new FindTheWords.WordGrid(this.gridParams); this.vocabulary = new FindTheWords.Vocabulary( this.options.vocabulary, this.options.behaviour.showVocabulary, this.options.l10n.wordListHeader ); this.registerDOMElements(); // responsive functionality this.on('resize', function () { const currentSize = this.elementSize; const currentVocMod = this.isVocModeBlock; this.calculateElementSize(); this.setVocabularyMode(); if (this.elementSize !== currentSize) { this.$puzzleContainer.empty(); this.grid.appendTo(this.$puzzleContainer, this.elementSize ); this.grid.drawGrid(MARGIN); // If there are already marked elements on the grid mark them if (!this.grid.options.gridActive) { this.grid.enableGrid(); this.grid.mark(this.vocabulary.getFound()); this.grid.disableGrid(); this.grid.mark(this.vocabulary.getSolved()); } else { this.grid.mark(this.vocabulary.getFound()); } this.registerGridEvents(); } // vocabulary adjustments on resize if (this.options.behaviour.showVocabulary) { if (currentVocMod !== this.isVocModeBlock ) { this.vocabulary.setMode(this.isVocModeBlock); if (this.isVocModeBlock) { this.$puzzleContainer.removeClass('puzzle-inline').addClass('puzzle-block'); } else { //initial update has to be done manually this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + VOCABULARY_INLINE_WIDTH}); this.$puzzleContainer.removeClass('puzzle-block').addClass('puzzle-inline'); } } } // Make the playarea just to fit its content if (! this.isVocModeBlock) { this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + 2}); } else { this.$playArea.css({'width': parseInt(this.$puzzleContainer.width()) + 2}); } }); } FindTheWords.prototype = Object.create(H5P.EventDispatcher.prototype); FindTheWords.prototype.constructor = FindTheWords; // private and all prototype function goes there /** * filterOrientations - Mapping of directions from semantics to those used by algorithm. * @param {Object} directions * @return {Object[]} */ const filterOrientations = function (directions) { return Object.keys(directions).filter(function (key) { return directions[key]; }); }; /** * registerDOMElements. */ FindTheWords.prototype.registerDOMElements = function () { const that = this; this.$playArea = $('
', { class: 'h5p-play-area' }); this.$taskDescription = $('
', { class: 'h5p-task-description', html: this.options.taskDescription, tabIndex: 0, }); // timer part this.$timer = $('
', { class: 'time-status', tabindex: 0, html: '' + this.options.l10n.timeSpent + ':' + '0:00' }); this.timer = new FindTheWords.Timer(this.$timer.find('.h5p-time-spent')); // counter part const counterText = that.options.l10n.found .replace('@found', '0') .replace('@totalWords', '' + this.vocabulary.words.length + ''); this.$counter = $('
', { class: 'counter-status', tabindex: 0, html: '
' + counterText + '
' }); this.counter = new FindTheWords.Counter(this.$counter.find('.h5p-counter')); // feedback plus progressBar this.$feedback = $('
', { class: 'feedback-element', tabindex: '0' }); this.$progressBar = UI.createScoreBar(this.vocabulary.words.length, 'scoreBarLabel'); // buttons section that.$submitButton = that.createButton('submit', 'check', that.options.l10n.check, that.gameSubmitted); if (this.options.behaviour.enableShowSolution) { this.$showSolutionButton = this.createButton('solution', 'eye', this.options.l10n.showSolution, that.showSolutions); } if (this.options.behaviour.enableRetry) { this.$retryButton = this.createButton('retry', 'undo', this.options.l10n.tryAgain, that.resetTask); } // container elements this.$gameContainer = $('
'); this.$puzzleContainer = $('
'); this.$vocabularyContainer = $('
'); this.$footerContainer = $('