/** @namespace H5P */ H5P.VideoVimeo = (function ($) { let numInstances = 0; /** * Vimeo video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function VimeoPlayer(sources, options, l10n) { const self = this; let player; // Since all the methods of the Vimeo Player SDK are promise-based, we keep // track of all relevant state variables so that we can implement the // H5P.Video API where all methods return synchronously. let buffered = 0; let currentQuality; let currentTextTrack; let currentTime = 0; let duration = 0; let isMuted = 0; let volume = 0; let playbackRate = 1; let qualities = []; let loadingFailedTimeout; let failedLoading = false; let ratio = 9/16; const LOADING_TIMEOUT_IN_SECONDS = 8; const id = `h5p-vimeo-${++numInstances}`; const $wrapper = $('
'); const $placeholder = $('
', { id: id, html: `
` }).appendTo($wrapper); /** * Create a new player with the Vimeo Player SDK. * * @private */ const createVimeoPlayer = async () => { if (!$placeholder.is(':visible') || player !== undefined) { return; } // Since the SDK is loaded asynchronously below, explicitly set player to // null (unlike undefined) which indicates that creation has begun. This // allows the guard statement above to be hit if this function is called // more than once. player = null; const Vimeo = await loadVimeoPlayerSDK(); const MIN_WIDTH = 200; const width = Math.max($wrapper.width(), MIN_WIDTH); const embedOptions = { url: sources[0].path, controls: options.controls ? true : false, responsive: true, dnt: true, // Hardcoded autoplay to false to avoid playing videos on init autoplay: false, loop: options.loop ? true : false, playsinline: true, quality: 'auto', width: width }; // Create a new player player = new Vimeo.Player(id, embedOptions); registerVimeoPlayerEventListeneners(player); // Failsafe timeout to handle failed loading of videos. // This seems to happen for private videos even though the SDK docs // suggests to catch PrivacyError when attempting play() loadingFailedTimeout = setTimeout(() => { failedLoading = true; removeLoadingIndicator(); $wrapper.html(`

${l10n.vimeoLoadingError}

`); $wrapper.css({ width: null, height: null }); self.trigger('resize'); self.trigger('error', l10n.vimeoLoadingError); }, LOADING_TIMEOUT_IN_SECONDS * 1000); } const removeLoadingIndicator = () => { $placeholder.find('div.h5p-video-loading').remove(); }; /** * Register event listeners on the given Vimeo player. * * @private * @param {Vimeo.Player} player */ const registerVimeoPlayerEventListeneners = (player) => { player.on('loaded', async () => { clearTimeout(loadingFailedTimeout); const videoDetails = await getVimeoVideoMetadata(player); const { tracks } = videoDetails; currentTextTrack = tracks.current; duration = videoDetails.duration; qualities = videoDetails.qualities; currentQuality = 'auto'; try { ratio = videoDetails.dimensions.height / videoDetails.dimensions.width; } catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ } removeLoadingIndicator(); if (options.startAt) { // Vimeo.Player doesn't have an option for setting start time upon // instantiation, so we instead perform an initial seek here. currentTime = await self.seek(options.startAt); } self.trigger('ready'); self.trigger('loaded'); self.trigger('captions', tracks.options); self.trigger('qualityChange', currentQuality); self.trigger('resize'); }); // Handle playback state changes. player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING)); player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED)); player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED)); // Track the percentage of video that has finished loading (buffered). player.on('progress', (data) => { buffered = data.percent * 100; }); // Track the current time. The update frequency may be browser-dependent, // according to the official docs: // https://developer.vimeo.com/player/sdk/reference#timeupdate player.on('timeupdate', (time) => { currentTime = time.seconds; }); }; /** * Get metadata about the video loaded in the given Vimeo player. * * Example resolved value: * * ``` * { * "duration": 39, * "qualities": [ * { * "name": "auto", * "label": "Auto" * }, * { * "name": "1080p", * "label": "1080p" * }, * { * "name": "720p", * "label": "720p" * } * ], * "dimensions": { * "width": 1920, * "height": 1080 * }, * "tracks": { * "current": { * "label": "English", * "value": "en" * }, * "options": [ * { * "label": "English", * "value": "en" * }, * { * "label": "Norsk bokmål", * "value": "nb" * } * ] * } * } * ``` * * @private * @param {Vimeo.Player} player * @returns {Promise} */ const getVimeoVideoMetadata = (player) => { // Create an object for easy lookup of relevant metadata const massageVideoMetadata = (data) => { const duration = data[0]; const qualities = data[1].map(q => ({ name: q.id, label: q.label })); const tracks = data[2].reduce((tracks, current) => { const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language); tracks.options.push(h5pVideoTrack); if (current.mode === 'showing') { tracks.current = h5pVideoTrack; } return tracks; }, { current: undefined, options: [] }); const dimensions = { width: data[3], height: data[4] }; return { duration, qualities, tracks, dimensions }; }; return Promise.all([ player.getDuration(), player.getQualities(), player.getTextTracks(), player.getVideoWidth(), player.getVideoHeight(), ]).then(data => massageVideoMetadata(data)); } /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = ($container) => { $container.addClass('h5p-vimeo').append($wrapper); createVimeoPlayer(); }; /** * Get list of available qualities. * * @public * @returns {Array} */ self.getQualities = () => { return qualities; }; /** * Get the current quality. * * @returns {String} Current quality identifier */ self.getQuality = () => { return currentQuality; }; /** * Set the playback quality. * * @public * @param {String} quality */ self.setQuality = async (quality) => { currentQuality = await player.setQuality(quality); self.trigger('qualityChange', currentQuality); }; /** * Start the video. * * @public */ self.play = async () => { if (!player) { self.on('ready', self.play); return; } try { await player.play(); } catch (error) { switch (error.name) { case 'PasswordError': // The video is password-protected self.trigger('error', l10n.vimeoPasswordError); break; case 'PrivacyError': // The video is private self.trigger('error', l10n.vimeoPrivacyError); break; default: self.trigger('error', l10n.unknownError); break; } } }; /** * Pause the video. * * @public */ self.pause = () => { if (player) { player.pause(); } }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = async (time) => { currentTime = await player.setCurrentTime(time); }; /** * @public * @returns {Number} Seconds elapsed since beginning of video */ self.getCurrentTime = () => { return currentTime; }; /** * @public * @returns {Number} Video duration in seconds */ self.getDuration = () => { return duration; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = () => { return buffered; }; /** * Mute the video. * * @public */ self.mute = async () => { isMuted = await player.setMuted(true); }; /** * Unmute the video. * * @public */ self.unMute = async () => { isMuted = await player.setMuted(false); }; /** * Whether the video is muted. * * @public * @returns {Boolean} True if the video is muted, false otherwise */ self.isMuted = () => { return isMuted; }; /** * Get the video player's current sound volume. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = () => { return volume; }; /** * Set the video player's sound volume. * * @public * @param {Number} level */ self.setVolume = async (level) => { volume = await player.setVolume(level); }; /** * Get list of available playback rates. * * @public * @returns {Array} Available playback rates */ self.getPlaybackRates = () => { return [0.5, 1, 1.5, 2]; }; /** * Get the current playback rate. * * @public * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ self.getPlaybackRate = () => { return playbackRate; }; /** * Set the current playback rate. * * @public * @param {Number} rate Must be one of available rates from getPlaybackRates */ self.setPlaybackRate = async (rate) => { playbackRate = await player.setPlaybackRate(rate); self.trigger('playbackRateChange', rate); }; /** * Set current captions track. * * @public * @param {H5P.Video.LabelValue} track Captions to display */ self.setCaptionsTrack = (track) => { if (!track) { return player.disableTextTrack().then(() => { currentTextTrack = null; }); } player.enableTextTrack(track.value).then(() => { currentTextTrack = track; }); }; /** * Get current captions track. * * @public * @returns {H5P.Video.LabelValue} */ self.getCaptionsTrack = () => { return currentTextTrack; }; self.on('resize', () => { if (failedLoading || !$wrapper.is(':visible')) { return; } if (player === undefined) { // Player isn't created yet. Try again. createVimeoPlayer(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: 'auto' }); const width = $wrapper[0].clientWidth; const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); // Validate height before setting if (height > 0) { // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); } }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ VimeoPlayer.canPlay = (sources) => { return getId(sources[0].path); }; /** * Find id of Vimeo video from given URL. * * @private * @param {String} url * @returns {String} Vimeo video ID */ const getId = (url) => { // https://stackoverflow.com/a/11660798 const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/); if (matches && matches[5]) { return matches[5]; } }; /** * Load the Vimeo Player SDK asynchronously. * * @private * @returns {Promise} Vimeo Player SDK object */ const loadVimeoPlayerSDK = async () => { if (window.Vimeo) { return await Promise.resolve(window.Vimeo); } return await new Promise((resolve, reject) => { const tag = document.createElement('script'); tag.src = 'https://player.vimeo.com/api/player.js'; tag.onload = () => resolve(window.Vimeo); tag.onerror = reject; document.querySelector('script').before(tag); }); }; return VimeoPlayer; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoVimeo); ; /** @namespace H5P */ H5P.VideoYouTube = (function ($) { /** * YouTube video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function YouTube(sources, options, l10n) { var self = this; var player; var playbackRate = 1; var id = 'h5p-youtube-' + numInstances; numInstances++; var $wrapper = $('
'); var $placeholder = $('
', { id: id, text: l10n.loading }).appendTo($wrapper); // Optional placeholder // var $placeholder = $('').appendTo($wrapper); /** * Use the YouTube API to create a new player * * @private */ var create = function () { if (!$placeholder.is(':visible') || player !== undefined) { return; } if (window.YT === undefined) { // Load API first loadAPI(create); return; } if (YT.Player === undefined) { return; } var width = $wrapper.width(); if (width < 200) { width = 200; } var loadCaptionsModule = true; var videoId = getId(sources[0].path); player = new YT.Player(id, { width: width, height: width * (9/16), videoId: videoId, playerVars: { origin: ORIGIN, // Hardcoded autoplay to false to avoid playing videos on init autoplay: 0, controls: options.controls ? 1 : 0, disablekb: options.controls ? 0 : 1, fs: 0, loop: options.loop ? 1 : 0, playlist: options.loop ? videoId : undefined, rel: 0, showinfo: 0, iv_load_policy: 3, wmode: "opaque", start: options.startAt, playsinline: 1 }, events: { onReady: function () { self.trigger('ready'); self.trigger('loaded'); }, onApiChange: function () { if (loadCaptionsModule) { loadCaptionsModule = false; // Always load captions player.loadModule('captions'); } var trackList; try { // Grab tracklist from player trackList = player.getOption('captions', 'tracklist'); } catch (err) {} if (trackList && trackList.length) { // Format track list into valid track options var trackOptions = []; for (var i = 0; i < trackList.length; i++) { trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode)); } // Captions are ready for loading self.trigger('captions', trackOptions); } }, onStateChange: function (state) { if (state.data > -1 && state.data < 4) { // Fix for keeping playback rate in IE11 if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) { // YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value player.setPlaybackRate(1); player.setPlaybackRate(playbackRate); } // End IE11 fix self.trigger('stateChange', state.data); } }, onPlaybackQualityChange: function (quality) { self.trigger('qualityChange', quality.data); }, onPlaybackRateChange: function (playbackRate) { self.trigger('playbackRateChange', playbackRate.data); }, onError: function (error) { var message; switch (error.data) { case 2: message = l10n.invalidYtId; break; case 100: message = l10n.unknownYtId; break; case 101: case 150: message = l10n.restrictedYt; break; default: message = l10n.unknownError + ' ' + error.data; break; } self.trigger('error', message); } } }); }; /** * Indicates if the video must be clicked for it to start playing. * For instance YouTube videos on iPad must be pressed to start playing. * * @public */ self.pressToPlay = navigator.userAgent.match(/iPad/i) ? true : false; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.addClass('h5p-youtube').append($wrapper); create(); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { if (!player || !player.getAvailableQualityLevels) { return; } var qualities = player.getAvailableQualityLevels(); if (!qualities.length) { return; // No qualities } // Add labels for (var i = 0; i < qualities.length; i++) { var quality = qualities[i]; var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n qualities[i] = { name: quality, label: LABELS[quality] }; } return qualities; }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { if (!player || !player.getPlaybackQuality) { return; } var quality = player.getPlaybackQuality(); return quality === 'unknown' ? undefined : quality; }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { if (!player || !player.setPlaybackQuality) { return; } player.setPlaybackQuality(quality); }; /** * Start the video. * * @public */ self.play = function () { if (!player || !player.playVideo) { self.on('ready', self.play); return; } player.playVideo(); }; /** * Pause the video. * * @public */ self.pause = function () { self.off('ready', self.play); if (!player || !player.pauseVideo) { return; } player.pauseVideo(); }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { if (!player || !player.seekTo) { return; } player.seekTo(time, true); }; /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { if (!player || !player.getCurrentTime) { return; } return player.getCurrentTime(); }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (!player || !player.getDuration) { return; } return player.getDuration(); }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { if (!player || !player.getVideoLoadedFraction) { return; } return player.getVideoLoadedFraction() * 100; }; /** * Turn off video sound. * * @public */ self.mute = function () { if (!player || !player.mute) { return; } player.mute(); }; /** * Turn on video sound. * * @public */ self.unMute = function () { if (!player || !player.unMute) { return; } player.unMute(); }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { if (!player || !player.isMuted) { return; } return player.isMuted(); }; /** * Return the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { if (!player || !player.getVolume) { return; } return player.getVolume(); }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { if (!player || !player.setVolume) { return; } player.setVolume(level); }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { if (!player || !player.getAvailablePlaybackRates) { return; } var playbackRates = player.getAvailablePlaybackRates(); if (!playbackRates.length) { return; // No rates, but the array should contain at least 1 } return playbackRates; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { if (!player || !player.getPlaybackRate) { return; } return player.getPlaybackRate(); }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { if (!player || !player.setPlaybackRate) { return; } playbackRate = Number(newPlaybackRate); player.setPlaybackRate(playbackRate); }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { player.setOption('captions', 'track', track ? {languageCode: track.value} : {}); }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { var track = player.getOption('captions', 'track'); return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null); }; // Respond to resize events by setting the YT player size. self.on('resize', function () { if (!$wrapper.is(':visible')) { return; } if (!player) { // Player isn't created yet. Try again. create(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: '100%' }); var width = $wrapper[0].clientWidth; var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16)); // Validate height before setting if (height > 0) { // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); player.setSize(width, height); } }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ YouTube.canPlay = function (sources) { return getId(sources[0].path); }; /** * Find id of YouTube video from given URL. * * @private * @param {String} url * @returns {String} YouTube video identifier */ var getId = function (url) { // Has some false positives, but should cover all regular URLs that people can find var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i); if (matches && matches[1]) { return matches[1]; } }; /** * Load the IFrame Player API asynchronously. */ var loadAPI = function (loaded) { if (window.onYouTubeIframeAPIReady !== undefined) { // Someone else is loading, hook in var original = window.onYouTubeIframeAPIReady; window.onYouTubeIframeAPIReady = function (id) { loaded(id); original(id); }; } else { // Load the API our self var tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onYouTubeIframeAPIReady = loaded; } }; /** @constant {Object} */ var LABELS = { highres: '2160p', // Old API support hd2160: '2160p', // (New API) hd1440: '1440p', hd1080: '1080p', hd720: '720p', large: '480p', medium: '360p', small: '240p', tiny: '144p', auto: 'Auto' }; /** @private */ var numInstances = 0; // Extract the current origin (used for security) var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/); ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0]; // ORIGIN = undefined is needed to support fetching file from device local storage return YouTube; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoYouTube); ; /** @namespace H5P */ H5P.VideoPanopto = (function ($) { /** * Panopto video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function Panopto(sources, options, l10n) { var self = this; self.volume = 100; var player; var playbackRate = 1; let canHasPlay; var id = 'h5p-panopto-' + numInstances; numInstances++; var $wrapper = $('
'); var $placeholder = $('
', { id: id, html: '
' + l10n.loading + '
' }).appendTo($wrapper); /** * Use the Panopto API to create a new player * * @private */ var create = function () { if (!$placeholder.is(':visible') || player !== undefined) { return; } if (window.EmbedApi === undefined) { // Load API first loadAPI(create); return; } var width = $wrapper.width(); if (width < 200) { width = 200; } const videoId = getId(sources[0].path); player = new EmbedApi(id, { width: width, height: width * (9/16), serverName: videoId[0], sessionId: videoId[1], videoParams: { // Optional interactivity: 'none', showtitle: false, autohide: true, offerviewer: false, autoplay: false, showbrand: false, start: 0, hideoverlay: !options.controls, }, events: { onIframeReady: function () { $placeholder.children(0).text(''); player.loadVideo(); self.trigger('containerLoaded'); self.trigger('resize'); // Avoid black iframe if loading is slow }, onReady: function () { self.trigger('loaded'); if (player.hasCaptions()) { const captions = []; const captionTracks = player.getCaptionTracks(); for (trackIndex in captionTracks) { captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex)); } // Select active track currentTrack = player.getSelectedCaptionTrack(); currentTrack = captions[currentTrack] ? captions[currentTrack] : null; self.trigger('captions', captions); } if (!canHasPlay) { self.pause(); // Only autoplay if play() has been called before load } }, onStateChange: function (state) { // TODO: Playback rate fix for IE11? if (state > -1 && state < 4) { self.trigger('stateChange', state); } }, onPlaybackRateChange: function () { self.trigger('playbackRateChange', self.getPlaybackRate()); }, onError: function (error) { if (error === ApiError.PlayWithSoundNotAllowed) { setTimeout(function () { self.unMute(); }, 10); } else { self.trigger('error', l10n.unknownError); } }, onLoginShown: function () { $placeholder.children().first().remove(); // Remove loading message self.trigger('loaded'); // Resize parent } } }); }; /** * Indicates if the video must be clicked for it to start playing. * This is always true for Panopto since all videos auto play. * * @public */ self.pressToPlay = true; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.addClass('h5p-panopto').append($wrapper); create(); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { // Not available for Panopto }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { // Not available for Panopto }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { // Not available for Panopto }; /** * Start the video. * * @public */ self.play = function () { canHasPlay = true; if (!player || !player.playVideo) { return; } player.playVideo(); }; /** * Pause the video. * * @public */ self.pause = function () { canHasPlay = false; if (!player || !player.pauseVideo) { return; } try { player.pauseVideo(); } catch (err) { // Swallow Panopto throwing an error. This has been seen in the authoring // tool if Panopto has been used inside Iv inside CP } }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { if (!player || !player.seekTo) { return; } player.seekTo(time); }; /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { if (!player || !player.getCurrentTime) { return; } return player.getCurrentTime(); }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (!player || !player.getDuration) { return; } return player.getDuration(); }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { // Not available for Panopto }; /** * Turn off video sound. * * @public */ self.mute = function () { if (!player || !player.muteVideo) { return; } player.muteVideo(); }; /** * Turn on video sound. * * @public */ self.unMute = function () { if (!player || !player.unmuteVideo) { return; } player.unmuteVideo(); // The volume is set to 0 when the browser prevents autoplay, // causing there to be no sound despite unmuting self.setVolume(self.volume); }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { if (!player || !player.isMuted) { return; } return player.isMuted(); }; /** * Return the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { if (!player || !player.getVolume) { return; } return player.getVolume() * 100; }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { if (!player || !player.setVolume) { return; } player.setVolume(level/100); self.volume = level; }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { return [0.25, 0.5, 1, 1.25, 1.5, 2]; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { if (!player || !player.getPlaybackRate) { return; } return player.getPlaybackRate(); }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { if (!player || !player.setPlaybackRate) { return; } player.setPlaybackRate(newPlaybackRate); }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { if (!track) { player.disableCaptions(); currentTrack = null; } else { player.enableCaptions(track.value + ''); currentTrack = track; } }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { return currentTrack; // No function for getting active caption track? }; // Respond to resize events by setting the player size. self.on('resize', function () { if (!$wrapper.is(':visible')) { return; } if (!player) { // Player isn't created yet. Try again. create(); return; } // Use as much space as possible $wrapper.css({ width: '100%', height: '100%' }); var width = $wrapper[0].clientWidth; var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16)); // Set size $wrapper.css({ width: width + 'px', height: height + 'px' }); const $iframe = $placeholder.children('iframe'); if ($iframe.length) { $iframe.attr('width', width); $iframe.attr('height', height); } }); let currentTrack; } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ Panopto.canPlay = function (sources) { return getId(sources[0].path); }; /** * Find id of YouTube video from given URL. * * @private * @param {String} url * @returns {String} Panopto video identifier */ var getId = function (url) { const matches = url.match(/^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/); if (matches && matches.length === 3) { return [matches[1], matches[2]]; } }; /** * Load the IFrame Player API asynchronously. */ var loadAPI = function (loaded) { if (window.onPanoptoEmbedApiReady !== undefined) { // Someone else is loading, hook in var original = window.onPanoptoEmbedApiReady; window.onPanoptoEmbedApiReady = function (id) { loaded(id); original(id); }; } else { // Load the API our self var tag = document.createElement('script'); tag.src = 'https://developers.panopto.com/scripts/embedapi.min.js'; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onPanoptoEmbedApiReady = loaded; } }; /** @private */ var numInstances = 0; return Panopto; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoPanopto); ; /** @namespace H5P */ H5P.VideoHtml5 = (function ($) { /** * HTML5 video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ function Html5(sources, options, l10n) { var self = this; /** * Small helper to ensure all video sources get the same cache buster. * * @private * @param {Object} source * @return {string} */ const getCrossOriginPath = function (source) { let path = H5P.getPath(source.path, self.contentId); if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) { path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster); } return path }; /** * Register track to video * * @param {Object} trackData Track object * @param {string} trackData.kind Kind of track * @param {Object} trackData.track Source path * @param {string} [trackData.label] Label of track * @param {string} [trackData.srcLang] Language code */ const addTrack = function (trackData) { // Skip invalid tracks if (!trackData.kind || !trackData.track.path) { return; } var track = document.createElement('track'); track.kind = trackData.kind; track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix. if (trackData.label) { track.label = trackData.label; } if (trackData.srcLang) { track.srcLang = trackData.srcLang; } return track; }; /** * Small helper to set the inital video source. * Useful if some of the loading happens asynchronously. * NOTE: Setting the crossOrigin must happen before any of the * sources(poster, tracks etc.) are loaded * * @private */ const setInitialSource = function () { if (qualities[currentQuality] === undefined) { return; } if (H5P.setSource !== undefined) { H5P.setSource(video, qualities[currentQuality].source, self.contentId) } else { // Backwards compatibility (H5P < v1.22) const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId); if (H5P.getCrossOrigin !== undefined) { var crossOrigin = H5P.getCrossOrigin(srcPath); video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous'); } video.src = srcPath; } // Add poster if provided if (options.poster) { video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix. } // Register tracks options.tracks.forEach(function (track, i) { var trackElement = addTrack(track); if (i === 0) { trackElement.default = true; } if (trackElement) { video.appendChild(trackElement); } }); }; /** * Displayed when the video is buffering * @private */ var $throbber = $('
', { 'class': 'h5p-video-loading' }); /** * Used to display error messages * @private */ var $error = $('
', { 'class': 'h5p-video-error' }); /** * Keep track of current state when changing quality. * @private */ var stateBeforeChangingQuality; var currentTimeBeforeChangingQuality; /** * Avoids firing the same event twice. * @private */ var lastState; /** * Keeps track whether or not the video has been loaded. * @private */ var isLoaded = false; /** * * @private */ var playbackRate = 1; var skipRateChange = false; // Create player var video = document.createElement('video'); // Sort sources into qualities var qualities = getQualities(sources, video); var currentQuality; numQualities = 0; for (let quality in qualities) { numQualities++; } if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) { H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) { if (qualities[chosenQuality] !== undefined) { currentQuality = chosenQuality; } setInitialSource(); }); } else { // Select quality and source currentQuality = getPreferredQuality(); if (currentQuality === undefined || qualities[currentQuality] === undefined) { // No preferred quality, pick the first. for (currentQuality in qualities) { if (qualities.hasOwnProperty(currentQuality)) { break; } } } setInitialSource(); } // Setting webkit-playsinline, which makes iOS 10 beeing able to play video // inside browser. video.setAttribute('webkit-playsinline', ''); video.setAttribute('playsinline', ''); video.setAttribute('preload', 'metadata'); // Remove buttons in Chrome's video player: let controlsList = 'nodownload'; if (options.disableFullscreen) { controlsList += ' nofullscreen'; } if (options.disableRemotePlayback) { controlsList += ' noremoteplayback'; } video.setAttribute('controlsList', controlsList); // Remove picture in picture as it interfers with other video players video.disablePictureInPicture = true; // Set options video.disableRemotePlayback = (options.disableRemotePlayback ? true : false); video.controls = (options.controls ? true : false); // Hardcoded autoplay to false to avoid playing videos on init video.autoplay = false; video.loop = (options.loop ? true : false); video.className = 'h5p-video'; video.style.display = 'block'; if (options.fit) { // Style is used since attributes with relative sizes aren't supported by IE9. video.style.width = '100%'; video.style.height = '100%'; } /** * Helps registering events. * * @private * @param {String} native Event name * @param {String} h5p Event name * @param {String} [arg] Optional argument */ var mapEvent = function (native, h5p, arg) { video.addEventListener(native, function () { switch (h5p) { case 'stateChange': if (lastState === arg) { return; // Avoid firing event twice. } var validStartTime = options.startAt && options.startAt > 0; if (arg === H5P.Video.PLAYING && validStartTime) { video.currentTime = options.startAt; delete options.startAt; } break; case 'loaded': isLoaded = true; if (stateBeforeChangingQuality !== undefined) { return; // Avoid loaded event when changing quality. } // Remove any errors if ($error.is(':visible')) { $error.remove(); } if (OLD_ANDROID_FIX) { var andLoaded = function () { video.removeEventListener('durationchange', andLoaded, false); // On Android seeking isn't ready until after play. self.trigger(h5p); }; video.addEventListener('durationchange', andLoaded, false); return; } break; case 'error': // Handle error and get message. arg = error(arguments[0], arguments[1]); break; case 'playbackRateChange': // Fix for keeping playback rate in IE11 if (skipRateChange) { skipRateChange = false; return; // Avoid firing event when changing back } if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional // Prevent change in playback rate not triggered by the user video.playbackRate = playbackRate; skipRateChange = true; return; } // End IE11 fix arg = self.getPlaybackRate(); break; } self.trigger(h5p, arg); }, false); }; /** * Handle errors from the video player. * * @private * @param {Object} code Error * @param {String} [message] * @returns {String} Human readable error message. */ var error = function (code, message) { if (code instanceof Event) { // No error code if (!code.target.error) { return ''; } switch (code.target.error.code) { case MediaError.MEDIA_ERR_ABORTED: message = l10n.aborted; break; case MediaError.MEDIA_ERR_NETWORK: message = l10n.networkFailure; break; case MediaError.MEDIA_ERR_DECODE: message = l10n.cannotDecode; break; case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: message = l10n.formatNotSupported; break; case MediaError.MEDIA_ERR_ENCRYPTED: message = l10n.mediaEncrypted; break; } } if (!message) { message = l10n.unknownError; } // Hide throbber $throbber.remove(); // Display error message to user $error.text(message).insertAfter(video); // Pass message to our error event return message; }; /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $container.append(video); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { // Create reverse list var options = []; for (var q in qualities) { if (qualities.hasOwnProperty(q)) { options.splice(0, 0, { name: q, label: qualities[q].label }); } } if (options.length < 2) { // Do not return if only one quality. return; } return options; }; /** * Get current playback quality. Not available until after play. * * @public * @returns {String} */ self.getQuality = function () { return currentQuality; }; /** * Set current playback quality. Not available until after play. * Listen to event "qualityChange" to check if successful. * * @public * @params {String} [quality] */ self.setQuality = function (quality) { if (qualities[quality] === undefined || quality === currentQuality) { return; // Invalid quality } // Keep track of last choice setPreferredQuality(quality); // Avoid multiple loaded events if changing quality multiple times. if (!stateBeforeChangingQuality) { // Keep track of last state stateBeforeChangingQuality = lastState; // Keep track of current time currentTimeBeforeChangingQuality = video.currentTime; // Seek and start video again after loading. var loaded = function () { video.removeEventListener('loadedmetadata', loaded, false); if (OLD_ANDROID_FIX) { var andLoaded = function () { video.removeEventListener('durationchange', andLoaded, false); // On Android seeking isn't ready until after play. self.seek(currentTimeBeforeChangingQuality); }; video.addEventListener('durationchange', andLoaded, false); } else { // Seek to current time. self.seek(currentTimeBeforeChangingQuality); } // Always play to get image. video.play(); if (stateBeforeChangingQuality !== H5P.Video.PLAYING) { // Do not resume playing video.pause(); } // Done changing quality stateBeforeChangingQuality = undefined; // Remove any errors if ($error.is(':visible')) { $error.remove(); } }; video.addEventListener('loadedmetadata', loaded, false); } // Keep track of current quality currentQuality = quality; self.trigger('qualityChange', currentQuality); // Display throbber self.trigger('stateChange', H5P.Video.BUFFERING); // Change source video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=). // Note: Optional tracks use same crossOrigin as the original. You cannot mix. // Remove poster so it will not show during quality change video.removeAttribute('poster'); }; /** * Starts the video. * * @public * @return {Promise|undefined} May return a Promise that resolves when * play has been processed. */ self.play = function () { if ($error.is(':visible')) { return; } if (!isLoaded) { // Make sure video is loaded before playing video.load(); } return video.play(); }; /** * Pauses the video. * * @public */ self.pause = function () { video.pause(); }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { if (lastState === undefined) { // Make sure we always play before we seek to get an image. // If not iOS devices will reset currentTime when pressing play. video.play(); video.pause(); } video.currentTime = time; }; /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { return video.currentTime; }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { if (isNaN(video.duration)) { return; } return video.duration; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { // Find buffer currently playing from var buffered = 0; for (var i = 0; i < video.buffered.length; i++) { var from = video.buffered.start(i); var to = video.buffered.end(i); if (video.currentTime > from && video.currentTime < to) { buffered = to; break; } } // To percentage return buffered ? (buffered / video.duration) * 100 : 0; }; /** * Turn off video sound. * * @public */ self.mute = function () { video.muted = true; }; /** * Turn on video sound. * * @public */ self.unMute = function () { video.muted = false; }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { return video.muted; }; /** * Returns the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { return video.volume * 100; }; /** * Set video sound level. * * @public * @param {Number} level Between 0 and 100. */ self.setVolume = function (level) { video.volume = level / 100; }; /** * Get list of available playback rates. * * @public * @returns {Array} available playback rates */ self.getPlaybackRates = function () { /* * not sure if there's a common rule about determining good speeds * using Google's standard options via a constant for setting */ var playbackRates = PLAYBACK_RATES; return playbackRates; }; /** * Get current playback rate. * * @public * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 */ self.getPlaybackRate = function () { return video.playbackRate; }; /** * Set current playback rate. * Listen to event "playbackRateChange" to check if successful. * * @public * @params {Number} suggested rate that may be rounded to supported values */ self.setPlaybackRate = function (newPlaybackRate) { playbackRate = newPlaybackRate; video.playbackRate = newPlaybackRate; }; /** * Set current captions track. * * @param {H5P.Video.LabelValue} Captions track to show during playback */ self.setCaptionsTrack = function (track) { for (var i = 0; i < video.textTracks.length; i++) { video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled'); } }; /** * Figure out which captions track is currently used. * * @return {H5P.Video.LabelValue} Captions track */ self.getCaptionsTrack = function () { for (var i = 0; i < video.textTracks.length; i++) { if (video.textTracks[i].mode === 'showing') { return new H5P.Video.LabelValue(video.textTracks[i].label, i); } } return null; }; // Register event listeners mapEvent('ended', 'stateChange', H5P.Video.ENDED); mapEvent('playing', 'stateChange', H5P.Video.PLAYING); mapEvent('pause', 'stateChange', H5P.Video.PAUSED); mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING); mapEvent('loadedmetadata', 'loaded'); mapEvent('canplay', 'canplay'); mapEvent('error', 'error'); mapEvent('ratechange', 'playbackRateChange'); if (!video.controls) { // Disable context menu(right click) to prevent controls. video.addEventListener('contextmenu', function (event) { event.preventDefault(); }, false); } // Display throbber when buffering/loading video. self.on('stateChange', function (event) { var state = event.data; lastState = state; if (state === H5P.Video.BUFFERING) { $throbber.insertAfter(video); } else { $throbber.remove(); } }); // Load captions after the video is loaded self.on('loaded', function () { nextTick(function () { var textTracks = []; for (var i = 0; i < video.textTracks.length; i++) { textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i)); } if (textTracks.length) { self.trigger('captions', textTracks); } }); }); // Alternative to 'canplay' event /*self.on('resize', function () { if (video.offsetParent === null) { return; } video.style.width = '100%'; video.style.height = '100%'; var width = video.clientWidth; var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth)); video.style.width = width + 'px'; video.style.height = height + 'px'; });*/ // Video controls are ready nextTick(function () { self.trigger('ready'); }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ Html5.canPlay = function (sources) { var video = document.createElement('video'); if (video.canPlayType === undefined) { return false; // Not supported } // Cycle through sources for (var i = 0; i < sources.length; i++) { var type = getType(sources[i]); if (type && video.canPlayType(type) !== '') { // We should be able to play this return true; } } return false; }; /** * Find source type. * * @private * @param {Object} source * @returns {String} */ var getType = function (source) { var type = source.mime; if (!type) { // Try to get type from URL var matches = source.path.match(/\.(\w+)$/); if (matches && matches[1]) { type = 'video/' + matches[1]; } } if (type && source.codecs) { // Add codecs type += '; codecs="' + source.codecs + '"'; } return type; }; /** * Sort sources into qualities. * * @private * @static * @param {Array} sources * @param {Object} video * @returns {Object} Quality mapping */ var getQualities = function (sources, video) { var qualities = {}; var qualityIndex = 1; var lastQuality; // Cycle through sources for (var i = 0; i < sources.length; i++) { var source = sources[i]; // Find and update type. var type = source.type = getType(source); // Check if we support this type var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== ''); if (!isPlayable) { continue; // We cannot play this source } if (source.quality === undefined) { /** * No quality metadata. Create a quality tag to separate multiple sources of the same type, * e.g. if two mp4 files with different quality has been uploaded */ if (lastQuality === undefined || qualities[lastQuality].source.type === type) { // Create a new quality tag source.quality = { name: 'q' + qualityIndex, label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n }; qualityIndex++; } else { /** * Assumes quality already exists in a different format. * Uses existing label for this quality. */ source.quality = qualities[lastQuality].source.quality; } } // Log last quality lastQuality = source.quality.name; // Look to see if quality exists var quality = qualities[lastQuality]; if (quality) { // We have a source with this quality. Check if we have a better format. if (source.mime.split('/')[1] === PREFERRED_FORMAT) { quality.source = source; } } else { // Add new source with quality. qualities[source.quality.name] = { label: source.quality.label, source: source }; } } return qualities; }; /** * Set preferred video quality. * * @private * @static * @param {String} quality Index of preferred quality */ var setPreferredQuality = function (quality) { try { localStorage.setItem('h5pVideoQuality', quality); } catch (err) { console.warn('Unable to set preferred video quality, localStorage is not available.'); } }; /** * Get preferred video quality. * * @private * @static * @returns {String} Index of preferred quality */ var getPreferredQuality = function () { // First check localStorage let quality; try { quality = localStorage.getItem('h5pVideoQuality'); } catch (err) { console.warn('Unable to retrieve preferred video quality from localStorage.'); } if (!quality) { try { // The fallback to old cookie solution var settings = document.cookie.split(';'); for (var i = 0; i < settings.length; i++) { var setting = settings[i].split('='); if (setting[0] === 'H5PVideoQuality') { quality = setting[1]; break; } } } catch (err) { console.warn('Unable to retrieve preferred video quality from cookie.'); } } return quality; }; /** * Helps schedule a task for the next tick. * @param {function} task */ var nextTick = function (task) { setTimeout(task, 0); }; /** @constant {Boolean} */ var OLD_ANDROID_FIX = false; /** @constant {Boolean} */ var PREFERRED_FORMAT = 'mp4'; /** @constant {Object} */ var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2]; if (navigator.userAgent.indexOf('Android') !== -1) { // We have Android, check version. var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/); if (version && version[1] && Number(version[1]) <= 534.30) { // Include fix for devices running the native Android browser. // (We don't know when video was fixed, so the number is just the lastest // native android browser we found.) OLD_ANDROID_FIX = true; } } else { if (navigator.userAgent.indexOf('Chrome') !== -1) { // If we're using chrome on a device that isn't Android, prefer the webm // format. This is because Chrome has trouble with some mp4 codecs. PREFERRED_FORMAT = 'webm'; } } return Html5; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoHtml5); ; /** @namespace H5P */ H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) { /** * The ultimate H5P video player! * * @class * @param {Object} parameters Options for this library. * @param {Object} parameters.visuals Visual options * @param {Object} parameters.playback Playback options * @param {Object} parameters.a11y Accessibility options * @param {Boolean} [parameters.startAt] Start time of video * @param {Number} id Content identifier */ function Video(parameters, id) { var self = this; self.contentId = id; // Ref youtube.js - ipad & youtube - issue self.pressToPlay = false; // Reference to the handler var handlerName = ''; // Initialize event inheritance H5P.EventDispatcher.call(self); // Default language localization parameters = $.extend(true, parameters, { l10n: { name: 'Video', loading: 'Video player loading...', noPlayers: 'Found no video players that supports the given video format.', noSources: 'Video source is missing.', aborted: 'Media playback has been aborted.', networkFailure: 'Network failure.', cannotDecode: 'Unable to decode media.', formatNotSupported: 'Video format not supported.', mediaEncrypted: 'Media encrypted.', unknownError: 'Unknown error.', vimeoPasswordError: 'Password-protected Vimeo videos are not supported.', vimeoPrivacyError: 'The Vimeo video cannot be used due to its privacy settings.', vimeoLoadingError: 'The Vimeo video could not be loaded.', invalidYtId: 'Invalid YouTube ID.', unknownYtId: 'Unable to find video with the given YouTube ID.', restrictedYt: 'The owner of this video does not allow it to be embedded.' } }); parameters.a11y = parameters.a11y || []; parameters.playback = parameters.playback || {}; parameters.visuals = $.extend(true, parameters.visuals, { disableFullscreen: false }); /** @private */ var sources = []; if (parameters.sources) { for (var i = 0; i < parameters.sources.length; i++) { // Clone to avoid changing of parameters. var source = $.extend(true, {}, parameters.sources[i]); // Create working URL without html entities. source.path = $cleaner.html(source.path).text(); sources.push(source); } } /** @private */ var tracks = []; parameters.a11y.forEach(function (track) { // Clone to avoid changing of parameters. var clone = $.extend(true, {}, track); // Create working URL without html entities if (clone.track && clone.track.path) { clone.track.path = $cleaner.html(clone.track.path).text(); tracks.push(clone); } }); /** * Handle autoplay. If autoplay is disabled, it will still autopause when * video is not visible. * * @param {*} $container */ const handleAutoPlayPause = function ($container) { // Keep the current state let state; self.on('stateChange', function(event) { state = event.data; }); // Keep record of autopauses. // I.e: we don't wanna autoplay if the user has excplicitly paused. self.autoPaused = true; new IntersectionObserver(function (entries) { const entry = entries[0]; // This video element became visible if (entry.isIntersecting) { // Autoplay if autoplay is enabled and it was not explicitly // paused by a user if (parameters.playback.autoplay && self.autoPaused) { self.autoPaused = false; self.play(); } } else if (state !== Video.PAUSED) { self.autoPaused = true; self.pause(); } }, { root: null, threshold: [0, 1] // Get events when it is shown and hidden }).observe($container.get(0)); }; /** * Attaches the video handler to the given container. * Inserts text if no handler is found. * * @public * @param {jQuery} $container */ self.attach = function ($container) { $container.addClass('h5p-video').html(''); if (self.appendTo !== undefined) { self.appendTo($container); // Avoid autoplaying in authoring tool if (window.H5PEditor === undefined) { handleAutoPlayPause($container); } } else if (sources.length) { $container.text(parameters.l10n.noPlayers); } else { $container.text(parameters.l10n.noSources); } }; /** * Get name of the video handler * * @public * @returns {string} */ self.getHandlerName = function() { return handlerName; }; // Resize the video when we know its aspect ratio self.on('loaded', function () { self.trigger('resize'); }); // Find player for video sources if (sources.length) { const options = { controls: parameters.visuals.controls, autoplay: false, loop: parameters.playback.loop, fit: parameters.visuals.fit, poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster, startAt: parameters.startAt || 0, tracks: tracks, disableRemotePlayback: parameters.visuals.disableRemotePlayback === true, disableFullscreen: parameters.visuals.disableFullscreen === true } var html5Handler; for (var i = 0; i < handlers.length; i++) { var handler = handlers[i]; if (handler.canPlay !== undefined && handler.canPlay(sources)) { handler.call(self, sources, options, parameters.l10n); handlerName = handler.name; return; } if (handler === H5P.VideoHtml5) { html5Handler = handler; handlerName = handler.name; } } // Fallback to trying HTML5 player if (html5Handler) { html5Handler.call(self, sources, options, parameters.l10n); } } } // Extends the event dispatcher Video.prototype = Object.create(H5P.EventDispatcher.prototype); Video.prototype.constructor = Video; // Player states /** @constant {Number} */ Video.ENDED = 0; /** @constant {Number} */ Video.PLAYING = 1; /** @constant {Number} */ Video.PAUSED = 2; /** @constant {Number} */ Video.BUFFERING = 3; /** * When video is queued to start * @constant {Number} */ Video.VIDEO_CUED = 5; // Used to convert between html and text, since URLs have html entities. var $cleaner = H5P.jQuery('
'); /** * Help keep track of key value pairs used by the UI. * * @class * @param {string} label * @param {string} value */ Video.LabelValue = function (label, value) { this.label = label; this.value = value; }; /** @constant {Boolean} */ Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false); return Video; })(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []); ; 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 e={46:e=>{e.exports=function(e,t){this.index=e,this.parent=t}},575:(e,t,i)=>{const n=i(46),r=H5P.EventDispatcher;function s(e,t){const i=this;r.call(i),i.children=[];var s=function(e){for(let t=e;t{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e=i(575),t=i.n(e),n=H5P.jQuery,r=H5P.EventDispatcher,s=H5P.JoubelUI,o=function(e){return e.concat.apply([],e)},a=function(e){return"function"==typeof e},l=null!==navigator.userAgent.match(/iPad/i),d=null!==navigator.userAgent.match(/iPad|iPod|iPhone/i),c=function(e,t){return-1!==e.indexOf(t)},p=function(e,t){return void 0!==e?e:t},h=13,u=27,m=32,f=function(e,t,i){e.click((function(e){t.call(i||this,e)})),e.keydown((function(e){c([h,m],e.which)&&(e.preventDefault(),t.call(i||this,e))}))},v=n("
");const g=function(){function e(e,t){this.$summarySlide=t,this.cp=e}return e.prototype.updateSummarySlide=function(e,t){var i=this,r=void 0===this.cp.editor&&void 0!==this.$summarySlide&&e>=this.cp.slides.length-1,o=!this.cp.showSummarySlide&&this.cp.hasAnswerElements;if(r){i.cp.presentation.keywordListEnabled&&i.cp.presentation.keywordListAlwaysShow&&i.cp.hideKeywords(),this.$summarySlide.children().remove();var a=i.cp.getSlideScores(t),l=i.outputScoreStats(a);if(n(l).appendTo(i.$summarySlide),!o){var d=i.totalScores(a);if(!isNaN(d.totalPercentage)){var c=s.createScoreBar(d.totalMaxScore,"","","");c.setScore(d.totalScore);var p=n(".h5p-summary-total-score",i.$summarySlide);c.appendTo(p),setTimeout((function(){p.append(n("
",{"aria-live":"polite",class:"hidden-but-read",html:i.cp.l10n.summary+". "+i.cp.l10n.accessibilityTotalScore.replace("@score",d.totalScore).replace("@maxScore",d.totalMaxScore)}))}),100)}if(1==i.cp.enableTwitterShare){var h=n(".h5p-summary-twitter-message",i.$summarySlide);this.addTwitterScoreLinkTo(h,d)}if(1==i.cp.enableFacebookShare){var u=n(".h5p-summary-facebook-message",i.$summarySlide);this.addFacebookScoreLinkTo(u,d)}if(1==i.cp.enableGoogleShare){var m=n(".h5p-summary-google-message",i.$summarySlide);this.addGoogleScoreLinkTo(m)}i.$summarySlide.find(".h5p-td > .h5p-slide-link").each((function(){var e=n(this);e.click((function(t){i.cp.jumpToSlide(parseInt(e.data("slide"),10)-1),t.preventDefault()}))}))}var f=n(".h5p-summary-footer",i.$summarySlide);this.cp.showSummarySlideSolutionButton&&s.createButton({class:"h5p-show-solutions",html:i.cp.l10n.showSolutions,on:{click:function(){i.toggleSolutionMode(!0)}},appendTo:f}),this.cp.showSummarySlideRetryButton&&s.createButton({class:"h5p-cp-retry-button",html:i.cp.l10n.retry,on:{click:function(){i.cp.resetTask()}},appendTo:f}),i.cp.hasAnswerElements&&s.createButton({class:"h5p-eta-export",html:i.cp.l10n.exportAnswers,on:{click:function(){H5P.ExportableTextArea.Exporter.run(i.cp.slides,i.cp.elementInstances)}},appendTo:f})}},e.prototype.outputScoreStats=function(e){if(void 0===e)return this.$summarySlide.addClass("h5p-summary-only-export"),'';var t,i=this,n=0,r=0,s="",o=0,a="";for(t=0;t]+)>)/gi,"")+" "+o+'%" data-slide="'+e[t].slide+'">'+i.cp.l10n.slide+" "+e[t].slide+": "+a.replace(/(<([^>]+)>)/gi,"")+'

'+o+"%

"+e[t].score+"/"+e[t].maxScore+"

",n+=e[t].score,r+=e[t].maxScore;this.cp.isSolutionMode||i.cp.triggerXAPICompleted(n,r);var l=i.cp.enableTwitterShare||i.cp.enableFacebookShare||i.cp.enableGoogleShare?''+i.cp.l10n.shareResult+"":"",d=1==i.cp.enableTwitterShare?'':"",c=1==i.cp.enableFacebookShare?'':"",p=1==i.cp.enableGoogleShare?'':"";return'
"+s+'
'+i.cp.l10n.slide+''+i.cp.l10n.score+"/"+i.cp.l10n.total.toLowerCase()+"
'+l+c+d+p+'

'+i.cp.l10n.totalScore+'

'},e.prototype.getSlideDescription=function(e){var t,i,n=this,r=n.cp.slides[e.slide-1].elements;if(e.indexes.length>1)t=n.cp.l10n.summaryMultipleTaskText;else if(void 0!==r[e.indexes[0]]&&r[0])if(i=r[e.indexes[0]].action,"function"==typeof n.cp.elementInstances[e.slide-1][e.indexes[0]].getTitle)t=n.cp.elementInstances[e.slide-1][e.indexes[0]].getTitle();else if(void 0!==i.library&&i.library){var s=i.library.split(" ")[0].split(".")[1].split(/(?=[A-Z])/),o="";s.forEach((function(e,t){0!==t&&(e=e.toLowerCase()),o+=e,t<=s.length-1&&(o+=" ")})),t=o}return t},e.prototype.addTwitterScoreLinkTo=function(e,t){var i=this,n=i.cp.twitterShareStatement||"",r=i.cp.twitterShareHashtags||"",s=i.cp.twitterShareUrl||"";s=s.replace("@currentpageurl",window.location.href),n=n.replace("@score",t.totalScore).replace("@maxScore",t.totalMaxScore).replace("@percentage",t.totalPercentage+"%").replace("@currentpageurl",window.location.href),r=r.trim().replace(" ",""),n=encodeURIComponent(n),r=encodeURIComponent(r),s=encodeURIComponent(s);var o="https://twitter.com/intent/tweet?";o+=n.length>0?"text="+n+"&":"",o+=s.length>0?"url="+s+"&":"",o+=r.length>0?"hashtags="+r:"";var a=window.innerWidth/2,l=window.innerHeight/2;e.attr("tabindex","0").attr("role","button"),f(e,(function(){return window.open(o,i.cp.l10n.shareTwitter,"width=800,height=300,left="+a+",top="+l),!1}))},e.prototype.addFacebookScoreLinkTo=function(e,t){var i=this,n=i.cp.facebookShareUrl||"",r=i.cp.facebookShareQuote||"";n=n.replace("@currentpageurl",window.location.href),r=r.replace("@currentpageurl",window.location.href).replace("@percentage",t.totalPercentage+"%").replace("@score",t.totalScore).replace("@maxScore",t.totalMaxScore),n=encodeURIComponent(n),r=encodeURIComponent(r);var s="https://www.facebook.com/sharer/sharer.php?";s+=n.length>0?"u="+n+"&":"",s+=r.length>0?"quote="+r:"";var o=window.innerWidth/2,a=window.innerHeight/2;e.attr("tabindex","0").attr("role","button"),f(e,(function(){return window.open(s,i.cp.l10n.shareFacebook,"width=800,height=300,left="+o+",top="+a),!1}))},e.prototype.addGoogleScoreLinkTo=function(e){var t=this,i=t.cp.googleShareUrl||"";i=i.replace("@currentpageurl",window.location.href),i=encodeURIComponent(i);var n="https://plus.google.com/share?";n+=i.length>0?"url="+i:"";var r=window.innerWidth/2,s=window.innerHeight/2;e.attr("tabindex","0").attr("role","button"),f(e,(function(){return window.open(n,t.cp.l10n.shareGoogle,"width=401,height=437,left="+r+",top="+s),!1}))},e.prototype.totalScores=function(e){if(void 0===e)return{totalScore:0,totalMaxScore:0,totalPercentage:0};var t,i=0,n=0;for(t=0;t\n ')).insertAfter(n).click((function(){s.close()})).children(".h5p-inner").click((function(){return!1})).end();f(d.find(".h5p-close"),(function(){return s.close()}));var c=d.find(".h5p-scroll-content");return c.append(e("
",{class:"h5p-cp-print-ingress",id:l,html:i.printIngress})),H5P.JoubelUI.createButton({html:i.printAllSlides,class:"h5p-cp-print-all-slides",click:function(){s.close(),r(!0)}}).appendTo(c),H5P.JoubelUI.createButton({html:i.printCurrentSlide,class:"h5p-cp-print-current-slide",click:function(){s.close(),r(!1)}}).appendTo(c),this.open=function(){setTimeout((function(){d.addClass("h5p-open"),H5P.jQuery(s).trigger("dialog-opened",[d])}),1)},this.close=function(){d.removeClass("h5p-open"),setTimeout((function(){d.remove()}),200)},this.open(),d},i}(H5P.jQuery);const y=b,S=function(e){const t=e.length;return function i(){const n=Array.prototype.slice.call(arguments,0);return n.length>=t?e.apply(null,n):function(){const e=Array.prototype.slice.call(arguments,0);return i.apply(null,n.concat(e))}}},w=S((function(e,t){t.forEach(e)})),x=(S((function(e,t){return t.map(e)})),S((function(e,t){return t.filter(e)}))),k=(S((function(e,t){return t.some(e)})),S((function(e,t){return-1!=t.indexOf(e)}))),T=S((function(e,t){return x((t=>!k(t,e)),t)})),C=S(((e,t)=>t.getAttribute(e))),E=S(((e,t,i)=>i.setAttribute(e,t))),P=S(((e,t)=>t.removeAttribute(e))),$=S(((e,t)=>t.hasAttribute(e))),I=(S(((e,t,i)=>i.getAttribute(e)===t)),S(((e,t)=>{const i=C(e,t);E(e,("true"!==i).toString(),t)})),S(((e,t)=>e.appendChild(t))),S(((e,t)=>t.querySelector(e))),S(((e,t)=>{return i=t.querySelectorAll(e),Array.prototype.slice.call(i);var i})),S(((e,t)=>e.removeChild(t))),S(((e,t)=>t.classList.contains(e))),S(((e,t)=>t.classList.add(e)))),A=S(((e,t)=>t.classList.remove(e))),B=I("hidden"),H=A("hidden"),M=(S(((e,t)=>(e?H:B)(t))),S(((e,t,i)=>{i.classList[t?"add":"remove"](e)})),P("tabindex")),L=(w(M),E("tabindex","0")),j=E("tabindex","-1"),O=$("tabindex");class D{constructor(e){Object.assign(this,{listeners:{},on:function(e,t,i){const n={listener:t,scope:i};return this.listeners[e]=this.listeners[e]||[],this.listeners[e].push(n),this},fire:function(e,t){return(this.listeners[e]||[]).every((function(e){return!1!==e.listener.call(e.scope||this,t)}))},propagate:function(e,t){let i=this;e.forEach((e=>t.on(e,(t=>i.fire(e,t)))))}}),this.plugins=e||[],this.elements=[],this.negativeTabIndexAllowed=!1,this.on("nextElement",this.nextElement,this),this.on("previousElement",this.previousElement,this),this.on("firstElement",this.firstElement,this),this.on("lastElement",this.lastElement,this),this.initPlugins()}addElement(e){this.elements.push(e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}insertElementAt(e,t){this.elements.splice(t,0,e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}removeElement(e){this.elements=T([e],this.elements),O(e)&&(this.setUntabbable(e),this.elements[0]&&this.setTabbable(this.elements[0])),this.firesEvent("removeElement",e)}count(){return this.elements.length}firesEvent(e,t){const i=this.elements.indexOf(t);return this.fire(e,{element:t,index:i,elements:this.elements,oldElement:this.tabbableElement})}nextElement({index:e}){const t=e===this.elements.length-1,i=this.elements[t?0:e+1];this.setTabbable(i),i.focus()}firstElement(){const e=this.elements[0];this.setTabbable(e),e.focus()}lastElement(){const e=this.elements[this.elements.length-1];this.setTabbable(e),e.focus()}setTabbableByIndex(e){const t=this.elements[e];t&&this.setTabbable(t)}setTabbable(e){w(this.setUntabbable.bind(this),this.elements),L(e),this.tabbableElement=e}setUntabbable(e){e!==document.activeElement&&(this.negativeTabIndexAllowed?j(e):M(e))}previousElement({index:e}){const t=0===e,i=this.elements[t?this.elements.length-1:e-1];this.setTabbable(i),i.focus()}useNegativeTabIndex(){this.negativeTabIndexAllowed=!0,this.elements.forEach((e=>{e.hasAttribute("tabindex")||j(e)}))}initPlugins(){this.plugins.forEach((function(e){void 0!==e.init&&e.init(this)}),this)}}class K{constructor(){this.selectability=!0}init(e){this.boundHandleKeyDown=this.handleKeyDown.bind(this),this.controls=e,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.removeKeyDownListener,this)}listenForKeyDown({element:e}){e.addEventListener("keydown",this.boundHandleKeyDown)}removeKeyDownListener({element:e}){e.removeEventListener("keydown",this.boundHandleKeyDown)}handleKeyDown(e){switch(e.which){case 27:this.close(e.target),e.preventDefault(),e.stopPropagation();break;case 35:this.lastElement(e.target),e.preventDefault(),e.stopPropagation();break;case 36:this.firstElement(e.target),e.preventDefault(),e.stopPropagation();break;case 13:case 32:this.select(e.target),e.preventDefault(),e.stopPropagation();break;case 37:case 38:this.hasChromevoxModifiers(e)||(this.previousElement(e.target),e.preventDefault(),e.stopPropagation());break;case 39:case 40:this.hasChromevoxModifiers(e)||(this.nextElement(e.target),e.preventDefault(),e.stopPropagation())}}hasChromevoxModifiers(e){return e.shiftKey||e.ctrlKey}previousElement(e){!1!==this.controls.firesEvent("beforePreviousElement",e)&&(this.controls.firesEvent("previousElement",e),this.controls.firesEvent("afterPreviousElement",e))}nextElement(e){!1!==this.controls.firesEvent("beforeNextElement",e)&&(this.controls.firesEvent("nextElement",e),this.controls.firesEvent("afterNextElement",e))}select(e){this.selectability&&!1!==this.controls.firesEvent("before-select",e)&&(this.controls.firesEvent("select",e),this.controls.firesEvent("after-select",e))}firstElement(e){!1!==this.controls.firesEvent("beforeFirstElement",e)&&(this.controls.firesEvent("firstElement",e),this.controls.firesEvent("afterFirstElement",e))}lastElement(e){!1!==this.controls.firesEvent("beforeLastElement",e)&&(this.controls.firesEvent("lastElement",e),this.controls.firesEvent("afterLastElement",e))}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}close(e){!1!==this.controls.firesEvent("before-close",e)&&(this.controls.firesEvent("close",e),this.controls.firesEvent("after-close",e))}}function F(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}var W="none",N="not-answered",z="answered",R="has-only-correct",Q="has-incorrect",U=function(e){function t(e){var t;this.cp=e,this.answeredLabels=(F(t={},N,this.cp.l10n.containsNotCompleted+"."),F(t,z,this.cp.l10n.containsCompleted+"."),F(t,R,this.cp.l10n.containsOnlyCorrect+"."),F(t,Q,this.cp.l10n.containsIncorrectAnswers+"."),F(t,W,""),t),this.initProgressbar(this.cp.slidesWithSolutions),this.initFooter(),this.initTaskAnsweredListener(),this.toggleNextAndPreviousButtonDisabled(this.cp.getCurrentSlideIndex())}return t.prototype.initTaskAnsweredListener=function(){var e=this;this.cp.elementInstances.forEach((function(t,i){t.filter((function(e){return a(e.on)})).forEach((function(t){t.on("xAPI",(function(t){var n=t.getVerb();if(c(["interacted","answered","attempted"],n)){var r=e.cp.slideHasAnsweredTask(i);e.setTaskAnswered(i,r)}else"completed"===n&&t.setVerb("answered");void 0===t.data.statement.context.extensions&&(t.data.statement.context.extensions={}),t.data.statement.context.extensions["http://id.tincanapi.com/extension/ending-point"]=i+1}))}))}))},t.prototype.initProgressbar=function(t){var i=this,n=i.cp.previousState&&i.cp.previousState.progress||0;this.progresbarKeyboardControls=new D([new K]),this.progresbarKeyboardControls.negativeTabIndexAllowed=!0,this.progresbarKeyboardControls.on("select",(function(t){i.displaySlide(e(t.element).data("slideNumber"))})),this.progresbarKeyboardControls.on("beforeNextElement",(function(e){return e.index!==e.elements.length-1})),this.progresbarKeyboardControls.on("beforePreviousElement",(function(e){return 0!==e.index})),void 0!==this.cp.progressbarParts&&this.cp.progressbarParts&&this.cp.progressbarParts.forEach((function(e){i.progresbarKeyboardControls.removeElement(e.children("a").get(0)),e.remove()})),i.cp.progressbarParts=[];for(var r=function(t){t.preventDefault();var n=e(this).data("slideNumber");i.progresbarKeyboardControls.setTabbableByIndex(n),i.displaySlide(n),i.cp.focus()},s=0;s",{class:"h5p-progressbar-part",id:"progressbar-part-"+s,role:"tab","aria-label":a,"aria-controls":"slide-"+s,"aria-selected":!1}).appendTo(i.cp.$progressbar),c=e("",{href:"#",html:''+a+"",tabindex:"-1"}).data("slideNumber",s).click(r).appendTo(l);if(this.progresbarKeyboardControls.addElement(c.get(0)),this.isSummarySlide(s)&&l.addClass("progressbar-part-summary-slide"),d||H5P.Tooltip(l.get(0),{position:"top"}),0===s&&l.addClass("h5p-progressbar-part-show"),s===n&&l.addClass("h5p-progressbar-part-selected").attr("aria-selected",!0),i.cp.progressbarParts.push(l),this.updateSlideTitle(s),this.cp.slides.length<=60&&o.elements&&o.elements.length>0){var p=t[s]&&t[s].length>0,h=!!(i.cp.previousState&&i.cp.previousState.answered&&i.cp.previousState.answered[s]);p&&(e("
",{class:"h5p-progressbar-part-has-task"}).appendTo(c),this.setTaskAnswered(s,h))}}},t.prototype.displaySlide=function(e){var t=this;this.cp.jumpToSlide(e,!1,(function(){var i=t.cp.getCurrentSlideIndex();t.updateSlideTitle(e,{isCurrent:!0}),t.updateSlideTitle(i,{isCurrent:!1}),t.toggleNextAndPreviousButtonDisabled(e)}))},t.prototype.createSlideTitle=function(e){var t=this.cp.slides[e];return t.keywords&&t.keywords.length>0?t.keywords[0].main:this.isSummarySlide(e)?this.cp.l10n.summary:this.cp.l10n.slide+" "+(e+1)},t.prototype.isSummarySlide=function(e){return!(void 0!==this.cp.editor||e!==this.cp.slides.length-1||!this.cp.showSummarySlide)},t.prototype.initFooter=function(){var t=this,i=this,n=this.cp.$footer,r=e("
",{class:"h5p-footer-left-adjusted"}).appendTo(n),s=e("
",{class:"h5p-footer-center-adjusted"}).appendTo(n),o=e("
",{role:"toolbar",class:"h5p-footer-right-adjusted"}).appendTo(n);this.cp.$keywordsButton=e("
",{class:"h5p-footer-button h5p-footer-toggle-keywords","aria-expanded":"false","aria-label":this.cp.l10n.showKeywords,role:"button",tabindex:"0",html:''}).appendTo(r),H5P.Tooltip(this.cp.$keywordsButton.get(0)),f(this.cp.$keywordsButton,(function(e){i.cp.presentation.keywordListAlwaysShow||(i.cp.toggleKeywords(),e.stopPropagation())})),!this.cp.presentation.keywordListAlwaysShow&&this.cp.initKeywords||this.cp.$keywordsButton.hide(),this.cp.presentation.keywordListEnabled||this.cp.$keywordsWrapper.add(this.$keywordsButton).hide(),this.updateFooterKeyword(0),this.cp.$prevSlideButton=e("
",{class:"h5p-footer-button h5p-footer-previous-slide","aria-label":this.cp.l10n.prevSlide,role:"button",tabindex:"-1","aria-disabled":"true"}).appendTo(s),new H5P.Tooltip(this.cp.$prevSlideButton.get(0),{position:"left"}),f(this.cp.$prevSlideButton,(function(){return t.cp.previousSlide(void 0,!1)}));var a=e("
",{class:"h5p-footer-slide-count"}).appendTo(s);this.cp.$footerCurrentSlide=e("
",{html:"1",class:"h5p-footer-slide-count-current",title:this.cp.l10n.currentSlide,"aria-hidden":"true"}).appendTo(a),this.cp.$footerCounter=e("
",{class:"hidden-but-read",html:this.cp.l10n.slideCount.replace("@index","1").replace("@total",this.cp.slides.length.toString())}).appendTo(s),e("
",{html:"/",class:"h5p-footer-slide-count-delimiter","aria-hidden":"true"}).appendTo(a),this.cp.$footerMaxSlide=e("
",{html:this.cp.slides.length,class:"h5p-footer-slide-count-max",title:this.cp.l10n.lastSlide,"aria-hidden":"true"}).appendTo(a),this.cp.$nextSlideButton=e("
",{class:"h5p-footer-button h5p-footer-next-slide","aria-label":this.cp.l10n.nextSlide,role:"button",tabindex:"0"}).appendTo(s),H5P.Tooltip(this.cp.$nextSlideButton.get(0),{position:"right"}),f(this.cp.$nextSlideButton,(function(){return t.cp.nextSlide(void 0,!1)})),void 0===this.cp.editor&&(this.cp.$exitSolutionModeButton=e("
",{role:"button",class:"h5p-footer-exit-solution-mode","aria-label":this.cp.l10n.solutionModeTitle,tabindex:"0"}).appendTo(o),H5P.Tooltip(this.cp.$exitSolutionModeButton.get(0)),f(this.cp.$exitSolutionModeButton,(function(){return i.cp.jumpToSlide(i.cp.slides.length-1)})),this.cp.enablePrintButton&&y.supported()&&(this.cp.$printButton=e("
",{class:"h5p-footer-button h5p-footer-print","aria-label":this.cp.l10n.printTitle,role:"button",tabindex:"0"}).appendTo(o),H5P.Tooltip(this.cp.$printButton.get(0)),f(this.cp.$printButton,(function(){return i.openPrintDialog()}))),H5P.fullscreenSupported&&(this.cp.$fullScreenButton=e("
",{class:"h5p-footer-button h5p-footer-toggle-full-screen","aria-label":this.cp.l10n.fullscreen,role:"button",tabindex:"0"}),H5P.Tooltip(this.cp.$fullScreenButton.get(0),{position:"left"}),f(this.cp.$fullScreenButton,(function(){return i.cp.toggleFullScreen()})),this.cp.$fullScreenButton.appendTo(o))),this.cp.$exitSolutionModeText=e("
",{html:"",class:"h5p-footer-exit-solution-mode-text"}).appendTo(this.cp.$exitSolutionModeButton)},t.prototype.openPrintDialog=function(){var t=this,i=e(".h5p-wrapper");y.showDialog(this.cp.l10n,i,(function(e){y.print(t.cp,i,e)})).children('[role="dialog"]').focus()},t.prototype.updateProgressBar=function(e,t,i){var n,r=this;for(n=0;nn?r.cp.progressbarParts[n].addClass("h5p-progressbar-part-show"):r.cp.progressbarParts[n].removeClass("h5p-progressbar-part-show");r.progresbarKeyboardControls.setTabbableByIndex(e),r.cp.progressbarParts[e].addClass("h5p-progressbar-part-selected").attr("aria-selected",!0).siblings().removeClass("h5p-progressbar-part-selected").attr("aria-selected",!1),void 0!==t?!i&&r.cp.editor:r.cp.progressbarParts.forEach((function(e,t){r.setTaskAnswered(t,!1)}))},t.prototype.setTaskAnswered=function(e,t){this.cp.progressbarParts[e].find(".h5p-progressbar-part-has-task").toggleClass("h5p-answered",t),this.updateSlideTitle(e,{state:t?z:N})},t.prototype.updateSlideTitle=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=t.state;this.setSlideTitle(e,{state:p(i,this.getAnsweredState(e))})},t.prototype.setSlideTitle=function(e,t){var i=t.state,n=void 0===i?W:i,r=this.cp.progressbarParts[e].find(".h5p-progressbar-part-title"),s=this.answeredLabels[n].replace("@slideName",this.createSlideTitle(e));r.html("".concat(s))},t.prototype.getAnsweredState=function(e){var t=this.cp.progressbarParts[e],i=this.slideHasInteraction(e),n=this.cp.slideHasAnsweredTask(e);return i?t.find(".h5p-is-correct").length>0?R:t.find(".h5p-is-wrong").length>0?Q:n?z:N:W},t.prototype.slideHasInteraction=function(e){return this.cp.progressbarParts[e].find(".h5p-progressbar-part-has-task").length>0},t.prototype.updateFooter=function(e){this.cp.$footerCurrentSlide.html(e+1),this.cp.$footerMaxSlide.html(this.cp.slides.length),this.cp.$footerCounter.html(this.cp.l10n.slideCount.replace("@index",(e+1).toString()).replace("@total",this.cp.slides.length.toString())),this.cp.isSolutionMode&&e===this.cp.slides.length-1?this.cp.$footer.addClass("summary-slide"):this.cp.$footer.removeClass("summary-slide"),this.toggleNextAndPreviousButtonDisabled(e),this.updateFooterKeyword(e)},t.prototype.toggleNextAndPreviousButtonDisabled=function(e){var t=this.cp.slides.length-1;this.cp.$prevSlideButton.attr("aria-disabled",(0===e).toString()),this.cp.$nextSlideButton.attr("aria-disabled",(e===t).toString()),this.cp.$prevSlideButton.attr("tabindex",0===e?"-1":"0"),this.cp.$nextSlideButton.attr("tabindex",e===t?"-1":"0")},t.prototype.updateFooterKeyword=function(e){var t=this.cp.slides[e],i="";t&&t.keywords&&t.keywords[0]&&(i=t.keywords[0].main),!this.cp.isEditor()&&this.cp.showSummarySlide&&e>=this.cp.slides.length-1&&(i=this.cp.l10n.summary),this.cp.$keywordsButton.children(".current-slide-title").html(p(i,""))},t}(H5P.jQuery);const G=U;var q=function(e){var t=e.presentation;t=n.extend(!0,{globalBackgroundSelector:{fillGlobalBackground:"",imageGlobalBackground:{}},slides:[{slideBackgroundSelector:{fillSlideBackground:"",imageSlideBackground:{}}}]},t);var i,r=function(t,i,n){var r=e.$slidesWrapper.children().filter(":not(.h5p-summary-slide)");void 0!==n&&(r=r.eq(n)),t&&""!==t?r.addClass("has-background").css("background-image","").css("background-color",t):i&&i.path&&r.addClass("has-background").css("background-color","").css("background-image","url("+H5P.getPath(i.path,e.contentId)+")")};i=t.globalBackgroundSelector,r(i.fillGlobalBackground,i.imageGlobalBackground),t.slides.forEach((function(e,t){var i=e.slideBackgroundSelector;i&&r(i.fillSlideBackground,i.imageSlideBackground,t)}))},X=function(e){return parseInt(e.dataset.index)},J=function(){function e(e){var t=this,i=e.l10n,n=e.currentIndex;this.l10n=i,this.state={currentIndex:p(n,0)},this.eventDispatcher=new r,this.controls=new D([new K]),this.controls.on("select",(function(e){t.onMenuItemSelect(X(e.element))})),this.controls.on("close",(function(){return t.eventDispatcher.trigger("close")})),this.menuElement=this.createMenuElement(),this.currentSlideMarkerElement=this.createCurrentSlideMarkerElement()}var t=e.prototype;return t.init=function(e){var t=this;return this.menuItemElements=e.map((function(e){return t.createMenuItemElement(e)})),this.menuItemElements.forEach((function(e){return t.menuElement.appendChild(e)})),this.menuItemElements.forEach((function(e){return t.controls.addElement(e)})),this.setCurrentSlideIndex(this.state.currentIndex),this.menuItemElements},t.on=function(e,t){this.eventDispatcher.on(e,t)},t.getElement=function(){return this.menuElement},t.removeAllMenuItemElements=function(){var e=this;this.menuItemElements.forEach((function(t){e.controls.removeElement(t),e.menuElement.removeChild(t)})),this.menuItemElements=[]},t.createMenuElement=function(){var e=this.menuElement=document.createElement("ol");return e.setAttribute("role","menu"),e.classList.add("list-unstyled"),e},t.createMenuItemElement=function(e){var t=this,i=document.createElement("li");return i.setAttribute("role","menuitem"),i.addEventListener("click",(function(e){t.onMenuItemSelect(X(e.currentTarget))})),this.applyConfigToMenuItemElement(i,e),i},t.applyConfigToMenuItemElement=function(e,t){e.innerHTML='
'.concat(t.subtitle,'
').concat(t.title,""),e.dataset.index=t.index},t.onMenuItemSelect=function(e){this.setCurrentSlideIndex(e),this.eventDispatcher.trigger("select",{index:e})},t.setCurrentSlideIndex=function(e){var t=this.getElementByIndex(this.menuItemElements,e);t&&(this.state.currentIndex=e,this.updateCurrentlySelected(this.menuItemElements,this.state),this.controls.setTabbable(t))},t.updateCurrentlySelected=function(e,t){var i=this;e.forEach((function(e){var n=t.currentIndex===X(e);e.classList.toggle("h5p-current",n),n&&e.appendChild(i.currentSlideMarkerElement)}))},t.scrollToKeywords=function(e){var t=this.getFirstElementAfter(e);if(t){var i=n(this.menuElement),r=i.scrollTop()+n(t).position().top-8;l?i.scrollTop(r):i.stop().animate({scrollTop:r},250)}},t.getFirstElementAfter=function(e){return this.menuItemElements.filter((function(t){return X(t)>=e}))[0]},t.getElementByIndex=function(e,t){return e.filter((function(e){return X(e)===t}))[0]},t.createCurrentSlideMarkerElement=function(){var e=document.createElement("div");return e.classList.add("hidden-but-read"),e.innerHTML=this.l10n.currentSlide,e},e}(),V="specified",Y="next",_="previous",Z=function(){function e(e,t){var i=this,s=e.title,o=e.goToSlide,a=void 0===o?1:o,l=e.invisible,d=e.goToSlideType,c=void 0===d?V:d,p=t.l10n,h=t.currentIndex;this.eventDispatcher=new r;var u="h5p-press-to-go",m=0;if(l)s=void 0,m=-1;else{if(!s)switch(c){case V:s=p.goToSlide.replace(":num",a.toString());break;case Y:s=p.goToSlide.replace(":num",p.nextSlide);break;case _:s=p.goToSlide.replace(":num",p.prevSlide)}u+=" h5p-visible"}var v=a-1;c===Y?v=h+1:c===_&&(v=h-1),this.$element=n("",{href:"#",class:u,tabindex:m,title:s}),f(this.$element,(function(e){i.eventDispatcher.trigger("navigate",v),e.preventDefault()}))}var t=e.prototype;return t.attach=function(e){e.html("").addClass("h5p-go-to-slide").append(this.$element)},t.on=function(e,t){this.eventDispatcher.on(e,t)},e}();const ee=function(e){var t=this;if(void 0===e.action)t.instance=new Z(e,{l10n:t.parent.parent.l10n,currentIndex:t.parent.index}),t.parent.parent.isEditor()||t.instance.on("navigate",(function(e){var i=e.data;t.parent.parent.jumpToSlide(i)}));else{var i;i=t.parent.parent.isEditor()?H5P.jQuery.extend(!0,{},e.action,t.parent.parent.elementsOverride):H5P.jQuery.extend(!0,e.action,t.parent.parent.elementsOverride);var n=t.parent.parent.elementInstances[t.parent.index]?t.parent.parent.elementInstances[t.parent.index].length:0;t.parent.parent.previousState&&t.parent.parent.previousState.answers&&t.parent.parent.previousState.answers[t.parent.index]&&t.parent.parent.previousState.answers[t.parent.index][n]&&(i.userDatas={state:t.parent.parent.previousState.answers[t.parent.index][n]}),i.params=i.params||{},t.instance=H5P.newRunnable(i,t.parent.parent.contentId,void 0,!0,{parent:t.parent.parent}),void 0!==t.instance.preventResize&&(t.instance.preventResize=!0)}void 0===t.parent.parent.elementInstances[t.parent.index]?t.parent.parent.elementInstances[t.parent.index]=[t.instance]:t.parent.parent.elementInstances[t.parent.index].push(t.instance),void 0!==t.instance.showCPComments||t.instance.isTask||void 0===t.instance.isTask&&void 0!==t.instance.showSolutions?(t.instance.coursePresentationIndexOnSlide=t.parent.parent.elementInstances[t.parent.index].length-1,void 0===t.parent.parent.slidesWithSolutions[t.parent.index]&&(t.parent.parent.slidesWithSolutions[t.parent.index]=[]),t.parent.parent.slidesWithSolutions[t.parent.index].push(t.instance)):e.solution&&(void 0===t.parent.parent.showCommentsAfterSolution[t.parent.index]&&(t.parent.parent.showCommentsAfterSolution[t.parent.index]=[]),t.parent.parent.showCommentsAfterSolution[t.parent.index].push(t.instance)),void 0!==t.instance.exportAnswers&&t.instance.exportAnswers&&(t.parent.parent.hasAnswerElements=!0),t.parent.parent.isTask||t.parent.parent.hideSummarySlide||(t.instance.isTask||void 0===t.instance.isTask&&void 0!==t.instance.showSolutions)&&(t.parent.parent.isTask=!0)};function te(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,n)}return i}function ie(e){for(var t=1;t
"};const se=re;const oe=function(e,t){var i=new H5P.ConfirmationDialog(e).appendTo(document.body);return i.getElement().classList.add("h5p-cp-confirmation-dialog"),i.show(),i};function ae(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var i=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null==i)return;var n,r,s=[],o=!0,a=!1;try{for(i=i.call(e);!(o=(n=i.next()).done)&&(s.push(n.value),!t||s.length!==t);o=!0);}catch(e){a=!0,r=e}finally{try{o||null==i.return||i.return()}finally{if(a)throw r}}return s}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return le(e,t);var i=Object.prototype.toString.call(e).slice(8,-1);"Object"===i&&e.constructor&&(i=e.constructor.name);if("Map"===i||"Set"===i)return Array.from(e);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return le(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function le(e,t){(null==t||t>e.length)&&(t=e.length);for(var i=0,n=new Array(t);i
';e.attr("role","application").addClass("h5p-course-presentation").html(r),this.$container=e,this.$slideAnnouncer=e.find(".h5p-current-slide-announcer"),this.$fullscreenAnnouncer=e.find(".h5p-fullscreen-announcer"),this.$slideTop=this.$slideAnnouncer.next(),this.$wrapper=e.children(".h5p-wrapper"),this.activeSurface&&this.$wrapper.addClass("h5p-course-presentation-active-surface"),this.$wrapper.focus((function(){i.initKeyEvents()})).blur((function(){void 0!==i.keydown&&(H5P.jQuery("body").unbind("keydown",i.keydown),delete i.keydown)})).click((function(e){var t=H5P.jQuery(e.target),n=i.belongsToTagName(e.target,["input","textarea","a","button"],e.currentTarget),r=-1!==e.target.tabIndex,s=t.closest(".h5p-popup-container"),o=0!==s.length;if(!n&&!r&&!i.editor)if(o){var a=t.closest("[tabindex]");1===a.closest(".h5p-popup-container").length?a.focus():s.find(".h5p-close-popup").focus()}else i.$wrapper.focus();i.presentation.keywordListEnabled&&!i.presentation.keywordListAlwaysShow&&i.presentation.keywordListAutoHide&&!t.is("textarea, .h5p-icon-pencil, span")&&i.hideKeywords()})),this.on("exitFullScreen",(function(){t.$footer.removeClass("footer-full-screen"),t.$fullScreenButton.attr("aria-label",t.l10n.fullscreen),t.$fullscreenAnnouncer.html(t.l10n.accessibilityExitedFullscreen)})),this.on("enterFullScreen",(function(){t.$fullscreenAnnouncer.html(t.l10n.accessibilityEnteredFullscreen)}));var s=parseInt(this.$wrapper.css("width"));this.width=0!==s?s:640;var o=parseInt(this.$wrapper.css("height"));this.height=0!==o?o:400,this.ratio=16/9,this.fontSize=16,this.$boxWrapper=this.$wrapper.find(".h5p-box-wrapper");var a,l=this.$boxWrapper.children(".h5p-presentation-wrapper");if(this.$slidesWrapper=l.children(".h5p-slides-wrapper"),this.$keywordsWrapper=l.children(".h5p-keywords-wrapper"),this.$progressbar=this.$wrapper.find(".h5p-progressbar"),this.$footer=this.$wrapper.children(".h5p-footer"),this.initKeywords=void 0===this.presentation.keywordListEnabled||!0===this.presentation.keywordListEnabled||void 0!==this.editor,this.activeSurface&&void 0===this.editor&&(this.initKeywords=!1,this.$boxWrapper.css("height","100%")),this.isSolutionMode=!1,this.createSlides(),this.elementsAttached[this.currentSlideIndex]=!0,this.showSummarySlide=!1,this.hideSummarySlide?this.showSummarySlide=!this.hideSummarySlide:this.slidesWithSolutions.forEach((function(e){i.showSummarySlide=e.length})),void 0===this.editor&&(this.showSummarySlide||this.hasAnswerElements)){var d={elements:[],keywords:[]};this.slides.push(d),(a=H5P.jQuery(se.createHTML(d)).appendTo(this.$slidesWrapper)).addClass("h5p-summary-slide"),this.isCurrentSlide(this.slides.length-1)&&(this.$current=a)}var c=this.getKeywordMenuConfig();c.length>0||this.isEditor()?(this.keywordMenu.init(c),this.keywordMenu.on("select",(function(e){return t.keywordClick(e.data.index)})),this.keywordMenu.on("close",(function(){return t.hideKeywords()})),this.keywordMenu.on("select",(function(){t.$currentKeyword=t.$keywords.children(".h5p-current")})),this.$keywords=n(this.keywordMenu.getElement()).appendTo(this.$keywordsWrapper),this.$currentKeyword=this.$keywords.children(".h5p-current"),void 0!==this.presentation.keywordListOpacity&&this.setKeywordsOpacity(this.presentation.keywordListOpacity),this.presentation.keywordListAlwaysShow&&this.showKeywords()):(this.$keywordsWrapper.remove(),this.initKeywords=!1),void 0===this.editor&&this.activeSurface?(this.$progressbar.add(this.$footer).remove(),H5P.fullscreenSupported&&(this.$fullScreenButton=H5P.jQuery("
",{class:"h5p-toggle-full-screen","aria-label":this.l10n.fullscreen,role:"button",tabindex:0,appendTo:this.$wrapper}),H5P.Tooltip(this.$fullScreenButton.get(0),{position:"left"}),f(this.$fullScreenButton,(function(){return i.toggleFullScreen()})))):(this.initTouchEvents(),this.navigationLine=new G(this),this.previousState&&this.previousState.progress||this.setSlideNumberAnnouncer(0,!1),this.summarySlideObject=new g(this,a)),new q(this),this.previousState&&this.previousState.progress&&this.jumpToSlide(this.previousState.progress,!1,null,!1,!0)},ce.prototype.belongsToTagName=function(e,t,i){if(!e)return!1;i=i||document.body,"string"==typeof t&&(t=[t]),t=t.map((function(e){return e.toLowerCase()}));var n=e.tagName.toLowerCase();return-1!==t.indexOf(n)||i!==e&&this.belongsToTagName(e.parentNode,t,i)},ce.prototype.updateKeywordMenuFromSlides=function(){this.keywordMenu.removeAllMenuItemElements();var e=this.getKeywordMenuConfig();return n(this.keywordMenu.init(e))},ce.prototype.getKeywordMenuConfig=function(){var e=this;return this.slides.map((function(t,i){return{title:e.createSlideTitle(t),subtitle:"".concat(e.l10n.slide," ").concat(i+1),index:i}})).filter((function(e){return null!==e.title}))},ce.prototype.createSlideTitle=function(e){var t=this.isEditor()?this.l10n.noTitle:null;return this.hasKeywords(e)?e.keywords[0].main:t},ce.prototype.isEditor=function(){return void 0!==this.editor},ce.prototype.hasKeywords=function(e){return void 0!==e.keywords&&e.keywords.length>0},ce.prototype.createSlides=function(){for(var e=this,t=0;t=e.maxScore;i.addClass(n?"h5p-is-correct":"h5p-is-wrong"),t.navigationLine.updateSlideTitle(e.slide-1)}})):this.progressbarParts.forEach((function(e){e.find(".h5p-progressbar-part-has-task").removeClass("h5p-is-correct").removeClass("h5p-is-wrong")}))},ce.prototype.toggleKeywords=function(){this[this.$keywordsWrapper.hasClass("h5p-open")?"hideKeywords":"showKeywords"]()},ce.prototype.hideKeywords=function(){this.$keywordsWrapper.hasClass("h5p-open")&&(void 0!==this.$keywordsButton&&(this.$keywordsButton.attr("aria-label",this.l10n.showKeywords),this.$keywordsButton.attr("aria-expanded","false"),this.$keywordsButton.focus()),this.$keywordsWrapper.removeClass("h5p-open"))},ce.prototype.showKeywords=function(){this.$keywordsWrapper.hasClass("h5p-open")||(void 0!==this.$keywordsButton&&(this.$keywordsButton.attr("aria-label",this.l10n.hideKeywords),this.$keywordsButton.attr("aria-expanded","true")),this.$keywordsWrapper.addClass("h5p-open"),this.presentation.keywordListAlwaysShow||this.$keywordsWrapper.find('li[tabindex="0"]').focus())},ce.prototype.setKeywordsOpacity=function(e){if(""!==this.$keywordsWrapper.css("background-color")){var t=ae(this.$keywordsWrapper.css("background-color").match(/\d+/g),3),i=t[0],n=t[1],r=t[2];this.$keywordsWrapper.css("background-color","rgba(".concat(i,", ").concat(n,", ").concat(r,", ").concat(e/100,")"))}},ce.prototype.fitCT=function(){void 0===this.editor&&this.$current.find(".h5p-ct").each((function(){for(var e=100,t=H5P.jQuery(this),i=t.parent().height();t.outerHeight()>i&&(e--,t.css({fontSize:e+"%",lineHeight:e+65+"%"}),!(e<0)););}))},ce.prototype.resize=function(){var e=this.$container.hasClass("h5p-fullscreen")||this.$container.hasClass("h5p-semi-fullscreen");if(!this.ignoreResize){this.$wrapper.css("width","auto");var t=this.$container.width(),i={};if(e){var n=this.$container.height();t/n>this.ratio&&(t=n*this.ratio,i.width=t+"px")}var r=t/this.width;i.height=t/this.ratio+"px",i.fontSize=this.fontSize*r+"px",void 0!==this.editor&&this.editor.setContainerEm(this.fontSize*r*.75),this.$wrapper.css(i),this.swipeThreshold=100*r;var s=this.elementInstances[this.$current.index()];if(void 0!==s)for(var o=this.slides[this.$current.index()].elements,a=0;a",{class:o}).css({left:e.x+"%",top:e.y+"%",width:e.width+"%",height:e.height+"%"}).appendTo(i.children('[role="document"]').first()),l=void 0===e.backgroundOpacity||0===e.backgroundOpacity;if(a.toggleClass("h5p-transparent",l),r){this.createInteractionButton(e,t).appendTo(a)}else{var d=e.action&&e.action.library?this.getLibraryTypePmz(e.action.library):"other",c=H5P.jQuery("
",{class:"h5p-element-outer ".concat(d,"-outer-element")}).css({background:"rgba(255,255,255,"+(void 0===e.backgroundOpacity?0:e.backgroundOpacity/100)+")"}).appendTo(a),p=H5P.jQuery("
",{class:"h5p-element-inner"}).appendTo(c);if(t.on("set-size",(function(e){for(var t in e.data)a.get(0).style[t]=e.data[t]})),t.attach(p),void 0!==e.action&&"H5P.InteractiveVideo"===e.action.library.substr(0,20)){var h=function(){t.$container.addClass("h5p-fullscreen"),t.controls.$fullscreen&&t.controls.$fullscreen.remove(),t.hasFullScreen=!0,t.controls.$play.hasClass("h5p-pause")?t.$controls.addClass("h5p-autohide"):t.enableAutoHide()};void 0!==t.controls?h():t.on("controls",h)}this.setOverflowTabIndex()}return void 0!==this.editor?this.editor.processElement(e,a,n,t):(e.solution&&this.addElementSolutionButton(e,t,a),this.hasAnswerElements=this.hasAnswerElements||void 0!==t.exportAnswers),a},ce.prototype.disableTabIndexes=function(){var e=this.$container.find(".h5p-popup-container");this.$tabbables=this.$container.find("a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]").filter((function(){var t=n(this),i=n.contains(e.get(0),t.get(0));if(t.data("tabindex"))return!0;if(!i){var r=t.attr("tabindex");return t.data("tabindex",r),t.attr("tabindex","-1"),!0}return!1}))},ce.prototype.restoreTabIndexes=function(){this.$tabbables&&this.$tabbables.each((function(){var e=n(this),t=e.data("tabindex");e.hasClass("ui-slider-handle")?(e.attr("tabindex",0),e.removeData("tabindex")):void 0!==t?(e.attr("tabindex",t),e.removeData("tabindex")):e.removeAttr("tabindex")}))},ce.prototype.createInteractionButton=function(e,t){var i=this,r=e.action.metadata?e.action.metadata.title:"";""===r&&(r=e.action.params&&e.action.params.contentName||e.action.library.split(" ")[0].split(".")[1]);var s=this.getLibraryTypePmz(e.action.library),o=n("
",{role:"button",tabindex:0,"aria-label":r,"aria-popup":!0,"aria-expanded":!1,class:"h5p-element-button h5p-element-button-".concat(e.buttonSize," ").concat(s,"-button")}),a=n('
');t.attach(a);var l="h5p-advancedtext"===s?{x:e.x,y:e.y}:null;return f(o,(function(){var e;o.attr("aria-expanded","true"),i.showInteractionPopup(t,o,a,s,(e=o,function(){return e.attr("aria-expanded","false")}),l)})),void 0!==e.action&&"H5P.InteractiveVideo"===e.action.library.substr(0,20)&&t.on("controls",(function(){t.controls.$fullscreen&&t.controls.$fullscreen.remove()})),o},ce.prototype.showInteractionPopup=function(e,t,i,n,r){var s=this,o=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,l=function(){e.trigger("resize")};this.isEditor()||(this.on("exitFullScreen",l),this.showPopup({popupContent:i,$focusOnClose:t,parentPosition:o,remove:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];e||i.detach(),s.off("exitFullScreen",l),r()},classes:n,instance:e,keepInDOM:"h5p-interactivevideo"===n}),H5P.trigger(e,"resize"),"h5p-image"===n&&this.resizePopupImage(i),setTimeout((function(){var e=i.find(":input").add(i.find("[tabindex]"));e.length?e[0].focus():(i.attr("tabindex",0),i.focus())}),200),a(e.setActivityStarted)&&a(e.getScore)&&e.setActivityStarted())},ce.prototype.getLibraryTypePmz=function(e){return(t=e.split(" ")[0],t.replace(/[\W]/g,"-")).toLowerCase();var t},ce.prototype.resizePopupImage=function(e){var t=Number(e.css("fontSize").replace("px","")),i=e.find("img"),n=function(i,n){if(!(n/t<18.5)){var r=i/n;n=18.5*t,e.css({width:n*r,height:n})}};i.height()?n(i.width(),i.height()):i.one("load",(function(){n(this.width,this.height)}))},ce.prototype.addElementSolutionButton=function(e,t,i){var r=this;t.showCPComments=function(){if(0===i.children(".h5p-element-solution").length&&(o=e.solution,v.html(o).text().trim()).length>0){var t=n("
",{role:"button",tabindex:0,title:r.l10n.solutionsButtonTitle,"aria-haspopup":"dialog","aria-expanded":!1,class:"h5p-element-solution"}).append('').appendTo(i),s={x:e.x,y:e.y};e.displayAsButton||(s.x+=e.width-4,s.y+=e.height-12),f(t,(function(i){r.showPopup({popupContent:e.solution,$focusOnClose:t,parentPosition:s,updateAriaExpanded:!0}),t.attr("aria-expanded",!0),i.stopPropagation()}))}var o},void 0!==e.alwaysDisplayComments&&e.alwaysDisplayComments&&t.showCPComments()},ce.prototype.showPopup=function(e){var t,i=this,r=e.popupContent,s=e.$focusOnClose,o=e.parentPosition,a=void 0===o?null:o,l=e.remove,d=e.classes,c=void 0===d?"h5p-popup-comment-field":d,p=e.instance,h=e.keepInDOM,m=void 0!==h&&h,v=e.updateAriaExpanded,g=this;this.popupId=void 0===this.popupId?0:this.popupId+1;var b,y=function(e){t?t=!1:(g.restoreTabIndexes(),s.focus(),v&&s.attr("aria-expanded",!1),void 0!==l&&setTimeout((function(){l(m)}),100),e.preventDefault(),b.addClass("h5p-animate"),b.find(".h5p-popup-container").addClass("h5p-animate"),setTimeout((function(){m?b.hide():b.remove()}),100))};if(m&&p&&g.popups[p.subContentId]&&(b=g.popups[p.subContentId]),void 0===b){var S=(b=n('
')).find(".h5p-popup-wrapper");r instanceof H5P.jQuery?S.append(r):S.html(r);var w="";S.children().each((function(e,t){t.setAttribute("id","popup-content-"+i.popupId+"-"+e),w+="popup-content-"+i.popupId+"-"+e+" "})),b.find(".h5p-popup-container").attr("aria-describedby",w),p&&p.subContentId&&(g.popups[p.subContentId]=b)}var x=b.find(".h5p-popup-container");return function(e,t,n){if(n){t.css({visibility:"hidden"}),e.prependTo(i.$wrapper);var r=t.height(),s=t.width(),o=e.height(),a=s*(100/e.width()),l=r*(100/o);if(a>50&&l>50)e.detach();else{a>l&&l<45&&(a=Math.sqrt(a*l),t.css({width:a+"%"}));a>90?a=90:a<22&&(a=22);var d=100-a-5,c=n.x;n.x>d?c=d:n.x<5&&(c=5);var p=100-(l=t.height()*(100/o))-10,h=n.y;n.y>p?h=p:n.y<10&&(h=10),e.detach(),t.css({left:c+"%",top:h+"%"})}}}(b,x,a),b.addClass("h5p-animate"),x.css({visibility:""}).addClass("h5p-animate"),0===b.parent().length?b.prependTo(this.$wrapper):b.show(),b.removeClass("h5p-animate").click(y).find(".h5p-popup-container").removeClass("h5p-animate").click((function(){t=!0})).keydown((function(e){e.which===u&&y(e)})).find(".h5p-close-popup").focus(),this.disableTabIndexes(),f(b.find(".h5p-close-popup"),(function(e){return y(e)})),b},ce.prototype.checkForSolutions=function(e){return void 0!==e.showSolutions||void 0!==e.showCPComments},ce.prototype.initKeyEvents=function(){if(void 0===this.keydown&&!this.activeSurface){var e=this,t=!1;this.keydown=function(i){t||((37!==i.keyCode&&33!==i.keyCode||!e.previousSlide(void 0,!1))&&(39!==i.keyCode&&34!==i.keyCode||!e.nextSlide(void 0,!1))||(i.preventDefault(),t=!0),t&&setTimeout((function(){t=!1}),300))},H5P.jQuery("body").keydown(this.keydown)}},ce.prototype.initTouchEvents=function(){var e,t,i,n,r,s,o=this,a=!1,l=!1,d=function(e){return{"-webkit-transform":e,"-moz-transform":e,"-ms-transform":e,transform:e}},c=d("");this.$slidesWrapper.bind("touchstart",(function(d){l=!1,i=e=d.originalEvent.touches[0].pageX,t=d.originalEvent.touches[0].pageY;var c=o.$slidesWrapper.width();n=0===o.currentSlideIndex?0:-c,r=o.currentSlideIndex+1>=o.slides.length?0:c,s=null,a=!0})).bind("touchmove",(function(c){var p=c.originalEvent.touches;a&&(o.$current.prev().addClass("h5p-touch-move"),o.$current.next().addClass("h5p-touch-move"),a=!1),i=p[0].pageX;var h=e-i;null===s&&(s=Math.abs(t-c.originalEvent.touches[0].pageY)>Math.abs(h)),1!==p.length||s||(c.preventDefault(),l||(h<0?o.$current.prev().css(d("translateX("+(n-h)+"px")):o.$current.next().css(d("translateX("+(r-h)+"px)")),o.$current.css(d("translateX("+-h+"px)"))))})).bind("touchend",(function(){if(!s){var t=e-i;if(t>o.swipeThreshold&&o.nextSlide(void 0,!1)||t<-o.swipeThreshold&&o.previousSlide(void 0,!1))return}o.$slidesWrapper.children().css(c).removeClass("h5p-touch-move")}))},ce.prototype.updateTouchPopup=function(e,t,i,n){if(arguments.length<=0)void 0!==this.touchPopup&&this.touchPopup.remove();else{var r="",s=.15;if(void 0!==this.$keywords&&void 0!==this.$keywords.children(":eq("+t+")").find("span").html())r+=this.$keywords.children(":eq("+t+")").find("span").html();else{var o=t+1;r+=this.l10n.slide+" "+o}void 0===this.editor&&t>=this.slides.length-1&&(r=this.l10n.showSolutions),void 0===this.touchPopup?this.touchPopup=H5P.jQuery("
",{class:"h5p-touch-popup"}).insertAfter(e):this.touchPopup.insertAfter(e),n-e.parent().height()*s<0?n=0:n-=e.parent().height()*s,this.touchPopup.css({"max-width":e.width()-i,left:i,top:n}),this.touchPopup.html(r)}},ce.prototype.previousSlide=function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=this.$current.prev();return!!i.length&&(t?this.processJumpToSlide(i.index(),e,!1):this.jumpToSlide(i.index(),e,null,!1))},ce.prototype.nextSlide=function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=this.$current.next();return!!i.length&&(t?this.processJumpToSlide(i.index(),e,!1):this.jumpToSlide(i.index(),e,null,!1))},ce.prototype.isCurrentSlide=function(e){return this.currentSlideIndex===e},ce.prototype.getCurrentSlideIndex=function(){return this.currentSlideIndex},ce.prototype.attachAllElements=function(){for(var e=this.$slidesWrapper.children(),t=0;t1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],s=arguments.length>4&&void 0!==arguments[4]&&arguments[4];if(this.standalone&&this.showSummarySlide&&e===this.slides.length-1&&!this.isSolutionMode&&this.isReportingEnabled&&!s){if(this.currentSlideIndex===this.slides.length-1)return!1;var o=oe({headerText:this.l10n.confirmDialogHeader,dialogText:this.l10n.confirmDialogText,confirmText:this.l10n.confirmDialogConfirmationText});o.on("canceled",(function(){return!1})),o.on("confirmed",(function(){t.processJumpToSlide(e,i,r),n&&n()}))}else this.processJumpToSlide(e,i,r),n&&n()},ce.prototype.setOverflowTabIndex=function(){void 0!==this.$current&&this.$current.find(".h5p-element-inner").each((function(){var e,t=n(this);this.classList.contains("h5p-table")&&(e=t.find(".h5p-table").outerHeight());var i=t.closest(".h5p-element-outer").innerHeight();void 0!==e&&null!==i&&e>i&&t.attr("tabindex",0)}))},ce.prototype.setSlideNumberAnnouncer=function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i="";if(!this.navigationLine)return i;var n=this.slides[e],r=n.keywords&&n.keywords.length>0;r&&!this.navigationLine.isSummarySlide(e)&&(i+=this.l10n.slide+" "+(e+1)+": "),i+=this.navigationLine.createSlideTitle(e),this.$slideAnnouncer.html(i),t&&this.$slideTop.focus()},ce.prototype.resetTask=function(){this.summarySlideObject.toggleSolutionMode(!1);for(var e=0;e