/* * flowplayer.js 3.2.12. The Flowplayer API * * Copyright 2009-2011 Flowplayer Oy * * This file is part of Flowplayer. * * Flowplayer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Flowplayer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Flowplayer. If not, see <http://www.gnu.org/licenses/>. * * Date: ${date} * Revision: ${revision} */ !function(){function h(p){console.log("$f.fireEvent",[].slice.call(p))}function l(r){if(!r||typeof r!="object"){return r}var p=new r.constructor();for(var q in r){if(r.hasOwnProperty(q)){p[q]=l(r[q])}}return p}function n(u,r){if(!u){return}var p,q=0,s=u.length;if(s===undefined){for(p in u){if(r.call(u[p],p,u[p])===false){break}}}else{for(var t=u[0];q<s&&r.call(t,q,t)!==false;t=u[++q]){}}return u}function c(p){return document.getElementById(p)}function j(r,q,p){if(typeof q!="object"){return r}if(r&&q){n(q,function(s,t){if(!p||typeof t!="function"){r[s]=t}})}return r}function o(t){var r=t.indexOf(".");if(r!=-1){var q=t.slice(0,r)||"*";var p=t.slice(r+1,t.length);var s=[];n(document.getElementsByTagName(q),function(){if(this.className&&this.className.indexOf(p)!=-1){s.push(this)}});return s}}function g(p){p=p||window.event;if(p.preventDefault){p.stopPropagation();p.preventDefault()}else{p.returnValue=false;p.cancelBubble=true}return false}function k(r,p,q){r[p]=r[p]||[];r[p].push(q)}function e(p){return p.replace(/&/g,"%26").replace(/&/g,"%26").replace(/=/g,"%3D")}function f(){return"_"+(""+Math.random()).slice(2,10)}var i=function(u,s,t){var r=this,q={},v={};r.index=s;if(typeof u=="string"){u={url:u}}j(this,u,true);n(("Begin*,Start,Pause*,Resume*,Seek*,Stop*,Finish*,LastSecond,Update,BufferFull,BufferEmpty,BufferStop").split(","),function(){var w="on"+this;if(w.indexOf("*")!=-1){w=w.slice(0,w.length-1);var x="onBefore"+w.slice(2);r[x]=function(y){k(v,x,y);return r}}r[w]=function(y){k(v,w,y);return r};if(s==-1){if(r[x]){t[x]=r[x]}if(r[w]){t[w]=r[w]}}});j(this,{onCuepoint:function(y,x){if(arguments.length==1){q.embedded=[null,y];return r}if(typeof y=="number"){y=[y]}var w=f();q[w]=[y,x];if(t.isLoaded()){t._api().fp_addCuepoints(y,s,w)}return r},update:function(x){j(r,x);if(t.isLoaded()){t._api().fp_updateClip(x,s)}var w=t.getConfig();var y=(s==-1)?w.clip:w.playlist[s];j(y,x,true)},_fireEvent:function(w,z,x,B){if(w=="onLoad"){n(q,function(C,D){if(D[0]){t._api().fp_addCuepoints(D[0],s,C)}});return false}B=B||r;if(w=="onCuepoint"){var A=q[z];if(A){return A[1].call(t,B,x)}}if(z&&"onBeforeBegin,onMetaData,onStart,onUpdate,onResume".indexOf(w)!=-1){j(B,z);if(z.metaData){if(!B.duration){B.duration=z.metaData.duration}else{B.fullDuration=z.metaData.duration}}}var y=true;n(v[w],function(){y=this.call(t,B,z,x)});return y}});if(u.onCuepoint){var p=u.onCuepoint;r.onCuepoint.apply(r,typeof p=="function"?[p]:p);delete u.onCuepoint}n(u,function(w,x){if(typeof x=="function"){k(v,w,x);delete u[w]}});if(s==-1){t.onCuepoint=this.onCuepoint}};var m=function(q,s,r,u){var p=this,t={},v=false;if(u){j(t,u)}n(s,function(w,x){if(typeof x=="function"){t[w]=x;delete s[w]}});j(this,{animate:function(z,A,y){if(!z){return p}if(typeof A=="function"){y=A;A=500}if(typeof z=="string"){var x=z;z={};z[x]=A;A=500}if(y){var w=f();t[w]=y}if(A===undefined){A=500}s=r._api().fp_animate(q,z,A,w);return p},css:function(x,y){if(y!==undefined){var w={};w[x]=y;x=w}s=r._api().fp_css(q,x);j(p,s);return p},show:function(){this.display="block";r._api().fp_showPlugin(q);return p},hide:function(){this.display="none";r._api().fp_hidePlugin(q);return p},toggle:function(){this.display=r._api().fp_togglePlugin(q);return p},fadeTo:function(z,y,x){if(typeof y=="function"){x=y;y=500}if(x){var w=f();t[w]=x}this.display=r._api().fp_fadeTo(q,z,y,w);this.opacity=z;return p},fadeIn:function(x,w){return p.fadeTo(1,x,w)},fadeOut:function(x,w){return p.fadeTo(0,x,w)},getName:function(){return q},getPlayer:function(){return r},_fireEvent:function(x,w,y){if(x=="onUpdate"){var A=r._api().fp_getPlugin(q);if(!A){return}j(p,A);delete p.methods;if(!v){n(A.methods,function(){var C=""+this;p[C]=function(){var D=[].slice.call(arguments);var E=r._api().fp_invoke(q,C,D);return E==="undefined"||E===undefined?p:E}});v=true}}var B=t[x];if(B){var z=B.apply(p,w);if(x.slice(0,1)=="_"){delete t[x]}return z}return p}})};function b(r,H,u){var x=this,w=null,E=false,v,t,G=[],z={},y={},F,s,q,D,p,B;j(x,{id:function(){return F},isLoaded:function(){return(w!==null&&w.fp_play!==undefined&&!E)},getParent:function(){return r},hide:function(I){if(I){r.style.height="0px"}if(x.isLoaded()){w.style.height="0px"}return x},show:function(){r.style.height=B+"px";if(x.isLoaded()){w.style.height=p+"px"}return x},isHidden:function(){return x.isLoaded()&&parseInt(w.style.height,10)===0},load:function(K){if(!x.isLoaded()&&x._fireEvent("onBeforeLoad")!==false){var I=function(){if(v&&!flashembed.isSupported(H.version)){r.innerHTML=""}if(K){K.cached=true;k(y,"onLoad",K)}flashembed(r,H,{config:u})};var J=0;n(a,function(){this.unload(function(L){if(++J==a.length){I()}})})}return x},unload:function(K){if(v.replace(/\s/g,"")!==""){if(x._fireEvent("onBeforeUnload")===false){if(K){K(false)}return x}E=true;try{if(w){if(w.fp_isFullscreen()){w.fp_toggleFullscreen()}w.fp_close();x._fireEvent("onUnload")}}catch(I){}var J=function(){w=null;r.innerHTML=v;E=false;if(K){K(true)}};if(/WebKit/i.test(navigator.userAgent)&&!/Chrome/i.test(navigator.userAgent)){setTimeout(J,0)}else{J()}}else{if(K){K(false)}}return x},getClip:function(I){if(I===undefined){I=D}return G[I]},getCommonClip:function(){return t},getPlaylist:function(){return G},getPlugin:function(I){var K=z[I];if(!K&&x.isLoaded()){var J=x._api().fp_getPlugin(I);if(J){K=new m(I,J,x);z[I]=K}}return K},getScreen:function(){return x.getPlugin("screen")},getControls:function(){return x.getPlugin("controls")._fireEvent("onUpdate")},getLogo:function(){try{return x.getPlugin("logo")._fireEvent("onUpdate")}catch(I){}},getPlay:function(){return x.getPlugin("play")._fireEvent("onUpdate")},getConfig:function(I){return I?l(u):u},getFlashParams:function(){return H},loadPlugin:function(L,K,N,M){if(typeof N=="function"){M=N;N={}}var J=M?f():"_";x._api().fp_loadPlugin(L,K,N,J);var I={};I[J]=M;var O=new m(L,null,x,I);z[L]=O;return O},getState:function(){return x.isLoaded()?w.fp_getState():-1},play:function(J,I){var K=function(){if(J!==undefined){x._api().fp_play(J,I)}else{x._api().fp_play()}};if(x.isLoaded()){K()}else{if(E){setTimeout(function(){x.play(J,I)},50)}else{x.load(function(){K()})}}return x},getVersion:function(){var J="flowplayer.js 3.2.12";if(x.isLoaded()){var I=w.fp_getVersion();I.push(J);return I}return J},_api:function(){if(!x.isLoaded()){throw"Flowplayer "+x.id()+" not loaded when calling an API method"}return w},setClip:function(I){n(I,function(J,K){if(typeof K=="function"){k(y,J,K);delete I[J]}else{if(J=="onCuepoint"){$f(r).getCommonClip().onCuepoint(I[J][0],I[J][1])}}});x.setPlaylist([I]);return x},getIndex:function(){return q},bufferAnimate:function(I){w.fp_bufferAnimate(I===undefined||I);return x},_swfHeight:function(){return w.clientHeight}});n(("Click*,Load*,Unload*,Keypress*,Volume*,Mute*,Unmute*,PlaylistReplace,ClipAdd,Fullscreen*,FullscreenExit,Error,MouseOver,MouseOut").split(","),function(){var I="on"+this;if(I.indexOf("*")!=-1){I=I.slice(0,I.length-1);var J="onBefore"+I.slice(2);x[J]=function(K){k(y,J,K);return x}}x[I]=function(K){k(y,I,K);return x}});n(("pause,resume,mute,unmute,stop,toggle,seek,getStatus,getVolume,setVolume,getTime,isPaused,isPlaying,startBuffering,stopBuffering,isFullscreen,toggleFullscreen,reset,close,setPlaylist,addClip,playFeed,setKeyboardShortcutsEnabled,isKeyboardShortcutsEnabled").split(","),function(){var I=this;x[I]=function(K,J){if(!x.isLoaded()){return x}var L=null;if(K!==undefined&&J!==undefined){L=w["fp_"+I](K,J)}else{L=(K===undefined)?w["fp_"+I]():w["fp_"+I](K)}return L==="undefined"||L===undefined?x:L}});x._fireEvent=function(R){if(typeof R=="string"){R=[R]}var S=R[0],P=R[1],N=R[2],M=R[3],L=0;if(u.debug){h(R)}if(!x.isLoaded()&&S=="onLoad"&&P=="player"){w=w||c(s);p=x._swfHeight();n(G,function(){this._fireEvent("onLoad")});n(z,function(T,U){U._fireEvent("onUpdate")});t._fireEvent("onLoad")}if(S=="onLoad"&&P!="player"){return}if(S=="onError"){if(typeof P=="string"||(typeof P=="number"&&typeof N=="number")){P=N;N=M}}if(S=="onContextMenu"){n(u.contextMenu[P],function(T,U){U.call(x)});return}if(S=="onPluginEvent"||S=="onBeforePluginEvent"){var I=P.name||P;var J=z[I];if(J){J._fireEvent("onUpdate",P);return J._fireEvent(N,R.slice(3))}return}if(S=="onPlaylistReplace"){G=[];var O=0;n(P,function(){G.push(new i(this,O++,x))})}if(S=="onClipAdd"){if(P.isInStream){return}P=new i(P,N,x);G.splice(N,0,P);for(L=N+1;L<G.length;L++){G[L].index++}}var Q=true;if(typeof P=="number"&&P<G.length){D=P;var K=G[P];if(K){Q=K._fireEvent(S,N,M)}if(!K||Q!==false){Q=t._fireEvent(S,N,M,K)}}n(y[S],function(){Q=this.call(x,P,N);if(this.cached){y[S].splice(L,1)}if(Q===false){return false}L++});return Q};function C(){if($f(r)){$f(r).getParent().innerHTML="";q=$f(r).getIndex();a[q]=x}else{a.push(x);q=a.length-1}B=parseInt(r.style.height,10)||r.clientHeight;F=r.id||"fp"+f();s=H.id||F+"_api";H.id=s;v=r.innerHTML;if(typeof u=="string"){u={clip:{url:u}}}u.playerId=F;u.clip=u.clip||{};if(r.getAttribute("href",2)&&!u.clip.url){u.clip.url=r.getAttribute("href",2)}if(u.clip.url){u.clip.url=e(u.clip.url)}t=new i(u.clip,-1,x);u.playlist=u.playlist||[u.clip];var J=0;n(u.playlist,function(){var M=this;if(typeof M=="object"&&M.length){M={url:""+M}}if(M.url){M.url=e(M.url)}n(u.clip,function(N,O){if(O!==undefined&&M[N]===undefined&&typeof O!="function"){M[N]=O}});u.playlist[J]=M;M=new i(M,J,x);G.push(M);J++});n(u,function(M,N){if(typeof N=="function"){if(t[M]){t[M](N)}else{k(y,M,N)}delete u[M]}});n(u.plugins,function(M,N){if(N){z[M]=new m(M,N,x)}});if(!u.plugins||u.plugins.controls===undefined){z.controls=new m("controls",null,x)}z.canvas=new m("canvas",null,x);v=r.innerHTML;function L(M){if(/iPad|iPhone|iPod/i.test(navigator.userAgent)&&!/.flv$/i.test(G[0].url)&&!K()){return true}if(!x.isLoaded()&&x._fireEvent("onBeforeClick")!==false){x.load()}return g(M)}function K(){return x.hasiPadSupport&&x.hasiPadSupport()}function I(){if(v.replace(/\s/g,"")!==""){if(r.addEventListener){r.addEventListener("click",L,false)}else{if(r.attachEvent){r.attachEvent("onclick",L)}}}else{if(r.addEventListener&&!K()){r.addEventListener("click",g,false)}x.load()}}setTimeout(I,0)}if(typeof r=="string"){var A=c(r);if(!A){throw"Flowplayer cannot access element: "+r}r=A;C()}else{C()}}var a=[];function d(p){this.length=p.length;this.each=function(r){n(p,r)};this.size=function(){return p.length};var q=this;for(name in b.prototype){q[name]=function(){var r=arguments;q.each(function(){this[name].apply(this,r)})}}}window.flowplayer=window.$f=function(){var q=null;var p=arguments[0];if(!arguments.length){n(a,function(){if(this.isLoaded()){q=this;return false}});return q||a[0]}if(arguments.length==1){if(typeof p=="number"){return a[p]}else{if(p=="*"){return new d(a)}n(a,function(){if(this.id()==p.id||this.id()==p||this.getParent()==p){q=this;return false}});return q}}if(arguments.length>1){var u=arguments[1],r=(arguments.length==3)?arguments[2]:{};if(typeof u=="string"){u={src:u}}u=j({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},u);if(typeof p=="string"){if(p.indexOf(".")!=-1){var t=[];n(o(p),function(){t.push(new b(this,l(u),l(r)))});return new d(t)}else{var s=c(p);return new b(s!==null?s:l(p),l(u),l(r))}}else{if(p){return new b(p,l(u),l(r))}}}return null};j(window.$f,{fireEvent:function(){var q=[].slice.call(arguments);var r=$f(q[0]);return r?r._fireEvent(q.slice(1)):null},addPlugin:function(p,q){b.prototype[p]=q;return $f},each:n,extend:j});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(r,q){if(!arguments.length||typeof arguments[0]=="number"){var p=[];this.each(function(){var s=$f(this);if(s){p.push(s)}});return arguments.length?p[arguments[0]]:new d(p)}return this.each(function(){$f(this,l(r),q?l(q):{})})}}}();!function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n='<object width="'+o.width+'" height="'+o.height+'" id="'+o.id+'" name="'+o.id+'"';if(o.cachebusting){o.src+=((o.src.indexOf("?")!=-1?"&":"?")+Math.random())}if(o.w3c||!h){n+=' data="'+o.src+'" type="application/x-shockwave-flash"'}else{n+=' classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'}n+=">";if(o.w3c||h){n+='<param name="movie" value="'+o.src+'" />'}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+='<param name="'+m+'" value="'+o[m]+'" />'}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+='<param name="flashvars" value=\''+p+"' />"}n+="</object>";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="<h2>Flash version "+n.version+" or greater is required</h2><h3>"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"</h3>"+(f.tagName=="A"?"<p>Click here to download latest version</p>":"<p>Download latest version from <a href='"+j+"'>here</a></p>");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.12"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}}();; /** @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 = $('<div/>'); var $placeholder = $('<div/>', { id: id, text: l10n.loading }).appendTo($wrapper); // Optional placeholder // var $placeholder = $('<iframe id="' + id + '" type="text/html" width="640" height="360" src="https://www.youtube.com/embed/' + getId(sources[0].path) + '?enablejsapi=1&origin=' + encodeURIComponent(ORIGIN) + '&autoplay=' + (options.autoplay ? 1 : 0) + '&controls=' + (options.controls ? 1 : 0) + '&disabledkb=' + (options.controls ? 0 : 1) + '&fs=0&loop=' + (options.loop ? 1 : 0) + '&rel=0&showinfo=0&iv_load_policy=3" frameborder="0"></iframe>').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, autoplay: options.autoplay ? 1 : 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; var player; var playbackRate = 1; var id = 'h5p-panopto-' + numInstances; numInstances++; var $wrapper = $('<div/>'); var $placeholder = $('<div/>', { id: id, html: '<div>' + l10n.loading + '</div>' }).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: !!options.autoplay, showbrand: false, start: 0, hideoverlay: !options.controls, }, events: { onIframeReady: function () { $placeholder.children(0).text(''); player.loadVideo(); }, 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); } self.pause(); }, 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 () { 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 () { if (!player || !player.playVideo) { return; } player.playVideo(); }; /** * Pause the video. * * @public */ self.pause = function () { 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(); }; /** * 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); }; /** * 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) { console.log('Disable captions'); player.disableCaptions(); currentTrack = null; } else { console.log('Set captions', track.value); 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 = $('<div/>', { 'class': 'h5p-video-loading' }); /** * Used to display error messages * @private */ var $error = $('<div/>', { '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); video.autoplay = (options.autoplay ? true : 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.VideoFlash = (function ($) { /** * Flash video player for H5P. * * @class * @param {Array} sources Video files to use * @param {Object} options Settings for the player */ function Flash(sources, options) { var self = this; // Player wrapper var $wrapper = $('<div/>', { 'class': 'h5p-video-flash', css: { width: '100%', height: '100%' } }); /** * Used to display error messages * @private */ var $error = $('<div/>', { 'class': 'h5p-video-error' }); /** * Keep track of current state when changing quality. * @private */ var stateBeforeChangingQuality; var currentTimeBeforeChangingQuality; // Sort sources into qualities //var qualities = getQualities(sources); var currentQuality; // Create player options var playerOptions = { buffering: true, clip: { url: sources[0].path, // getPreferredQuality(), autoPlay: options.autoplay, autoBuffering: true, scaling: 'fit', onSeek: function () { if (stateBeforeChangingQuality) { // ???? } }, onMetaData: function () { setTimeout(function () { if (stateBeforeChangingQuality !== undefined) { fp.seek(currentTimeBeforeChangingQuality); if (stateBeforeChangingQuality === H5P.Video.PLAYING) { // Resume play fp.play(); } // Done changing quality stateBeforeChangingQuality = undefined; // Remove any errors if ($error.is(':visible')) { $error.remove(); } } else { self.trigger('ready'); self.trigger('loaded'); } }, 0); // Run on next tick }, onBegin: function () { self.trigger('stateChange', H5P.Video.PLAYING); }, onResume: function () { self.trigger('stateChange', H5P.Video.PLAYING); }, onPause: function () { self.trigger('stateChange', H5P.Video.PAUSED); }, onFinish: function () { self.trigger('stateChange', H5P.Video.ENDED); }, onError: function (code, message) { console.log('ERROR', code, message); // TODO self.trigger('error', message); } }, plugins: { controls: null }, play: null, // Disable overlay controls onPlaylistReplace: function () { that.playlistReplaced(); } }; if (options.controls) { playerOptions.plugins.controls = {}; delete playerOptions.play; } var fp = flowplayer($wrapper[0], { src: "http://releases.flowplayer.org/swf/flowplayer-3.2.16.swf", wmode: "opaque" }, playerOptions); /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ self.appendTo = function ($container) { $wrapper.appendTo($container); }; /** * Get list of available qualities. Not available until after play. * * @public * @returns {Array} */ self.getQualities = function () { return; }; /** * 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; } // Keep track of current quality currentQuality = quality; self.trigger('qualityChange', currentQuality); // Display throbber self.trigger('stateChange', H5P.Video.BUFFERING); // Change source fp.setClip(qualities[quality].source.path); fp.startBuffering(); }; /** * Starts the video. * * @public */ self.play = function () { if ($error.is(':visible')) { return; } fp.play(); }; /** * Pauses the video. * * @public */ self.pause = function () { fp.pause(); }; /** * Seek video to given time. * * @public * @param {Number} time */ self.seek = function (time) { fp.seek(time); }; /** * Get elapsed time since video beginning. * * @public * @returns {Number} */ self.getCurrentTime = function () { return fp.getTime(); }; /** * Get total video duration time. * * @public * @returns {Number} */ self.getDuration = function () { return fp.getClip().metaData.duration; }; /** * Get percentage of video that is buffered. * * @public * @returns {Number} Between 0 and 100 */ self.getBuffered = function () { return fp.getClip().buffer; }; /** * Turn off video sound. * * @public */ self.mute = function () { fp.mute(); }; /** * Turn on video sound. * * @public */ self.unMute = function () { fp.unmute(); }; /** * Check if video sound is turned on or off. * * @public * @returns {Boolean} */ self.isMuted = function () { return fp.muted; }; /** * Returns the video sound level. * * @public * @returns {Number} Between 0 and 100. */ self.getVolume = function () { return fp.volumeLevel * 100; }; /** * Set video sound level. * * @public * @param {Number} volume Between 0 and 100. */ self.setVolume = function (level) { fp.volume(level / 100); }; // Handle resize events self.on('resize', function () { var $object = H5P.jQuery(fp.getParent()).children('object'); var clip = fp.getClip(); if (clip !== undefined) { $object.css('height', $object.width() * (clip.metaData.height / clip.metaData.width)); } }); } /** * Check to see if we can play any of the given sources. * * @public * @static * @param {Array} sources * @returns {Boolean} */ Flash.canPlay = function (sources) { // Cycle through sources for (var i = 0; i < sources.length; i++) { if (sources[i].mime === 'video/mp4' || /\.mp4$/.test(sources[i].mime)) { return true; // We only play mp4 } } }; return Flash; })(H5P.jQuery); // Register video handler H5P.videoHandlers = H5P.videoHandlers || []; H5P.videoHandlers.push(H5P.VideoFlash); ; /** @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.', 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); } }); /** * 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); } 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: parameters.playback.autoplay, 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('<div/>'); /** * 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 || {}; /** * Constructor. * * @param {Object} params Options for this library. * @param {Number} id Content identifier * @returns {undefined} */ (function ($) { H5P.Image = function (params, id, extras) { H5P.EventDispatcher.call(this); this.extras = extras; if (params.file === undefined || !(params.file instanceof Object)) { this.placeholder = true; } else { this.source = H5P.getPath(params.file.path, id); this.width = params.file.width; this.height = params.file.height; } this.alt = (!params.decorative && params.alt !== undefined) ? this.stripHTML(this.htmlDecode(params.alt)) : ''; if (params.title !== undefined) { this.title = this.stripHTML(this.htmlDecode(params.title)); } }; H5P.Image.prototype = Object.create(H5P.EventDispatcher.prototype); H5P.Image.prototype.constructor = H5P.Image; /** * Wipe out the content of the wrapper and put our HTML in it. * * @param {jQuery} $wrapper * @returns {undefined} */ H5P.Image.prototype.attach = function ($wrapper) { var self = this; var source = this.source; if (self.$img === undefined) { if(self.placeholder) { self.$img = $('<div>', { width: '100%', height: '100%', class: 'h5p-placeholder', title: this.title === undefined ? '' : this.title, on: { load: function () { self.trigger('loaded'); } } }); } else { self.$img = $('<img>', { width: '100%', height: '100%', src: source, alt: this.alt, title: this.title === undefined ? '' : this.title, on: { load: function () { self.trigger('loaded'); } } }); } } $wrapper.addClass('h5p-image').html(self.$img); }; /** * Retrieve decoded HTML encoded string. * * @param {string} input HTML encoded string. * @returns {string} Decoded string. */ H5P.Image.prototype.htmlDecode = function (input) { const dparser = new DOMParser().parseFromString(input, 'text/html'); return dparser.documentElement.textContent; }; /** * Retrieve string without HTML tags. * * @param {string} input Input string. * @returns {string} Output string. */ H5P.Image.prototype.stripHTML = function (html) { const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; return H5P.Image; }(H5P.jQuery)); ; 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 = $('<div>', { 'class': 'joubel-help-text-dialog-box', 'role': 'dialog', 'aria-labelledby': headerId, 'aria-describedby': helpTextId }); $('<div>', { 'class': 'joubel-help-text-dialog-background' }).appendTo($helpTextDialogBox); var $helpTextDialogContainer = $('<div>', { 'class': 'joubel-help-text-dialog-container' }).appendTo($helpTextDialogBox); $('<div>', { 'class': 'joubel-help-text-header', 'id': headerId, 'role': 'header', 'html': header }).appendTo($helpTextDialogContainer); $('<div>', { 'class': 'joubel-help-text-body', 'id': helpTextId, 'html': message, 'role': 'document', 'tabindex': 0 }).appendTo($helpTextDialogContainer); var handleClose = function () { $helpTextDialogBox.remove(); self.trigger('closed'); }; var $closeButton = $('<div>', { 'class': 'joubel-help-text-remove', 'role': 'button', 'title': closeButtonTitle, 'tabindex': 1, 'click': handleClose, 'keydown': function (event) { // 32 - space, 13 - enter if ([32, 13].indexOf(event.which) !== -1) { event.preventDefault(); handleClose(); } } }).appendTo($helpTextDialogContainer); /** * Get the DOM element * @return {HTMLElement} */ self.getElement = function () { return $helpTextDialogBox; }; self.focus = function () { $closeButton.focus(); }; } JoubelHelpTextDialog.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelHelpTextDialog.prototype.constructor = JoubelHelpTextDialog; return JoubelHelpTextDialog; }(H5P.jQuery)); ; var H5P = H5P || {}; /** * Class responsible for creating auto-disappearing dialogs */ H5P.JoubelMessageDialog = (function ($) { /** * Display a pop-up containing a message. * * @param {H5P.jQuery} $container The container which message dialog will be appended to * @param {string} message The message * @return {H5P.jQuery} */ function JoubelMessageDialog ($container, message) { var timeout; var removeDialog = function () { $warning.remove(); clearTimeout(timeout); $container.off('click.messageDialog'); }; // Create warning popup: var $warning = $('<div/>', { 'class': 'joubel-message-dialog', text: message }).appendTo($container); // Remove after 3 seconds or if user clicks anywhere in $container: timeout = setTimeout(removeDialog, 3000); $container.on('click.messageDialog', removeDialog); return $warning; } return JoubelMessageDialog; })(H5P.jQuery); ; var H5P = H5P || {}; /** * Class responsible for creating a circular progress bar */ H5P.JoubelProgressCircle = (function ($) { /** * Constructor for the Progress Circle * * @param {Number} number The amount of progress to display * @param {string} progressColor Color for the progress meter * @param {string} backgroundColor Color behind the progress meter */ function ProgressCircle(number, progressColor, fillColor, backgroundColor) { progressColor = progressColor || '#1a73d9'; fillColor = fillColor || '#f0f0f0'; backgroundColor = backgroundColor || '#ffffff'; var progressColorRGB = this.hexToRgb(progressColor); //Verify number try { number = Number(number); if (number === '') { throw 'is empty'; } if (isNaN(number)) { throw 'is not a number'; } } catch (e) { number = 'err'; } //Draw circle if (number > 100) { number = 100; } // We can not use rgba, since they will stack on top of each other. // Instead we create the equivalent of the rgba color // and applies this to the activeborder and background color. var progressColorString = 'rgb(' + parseInt(progressColorRGB.r, 10) + ',' + parseInt(progressColorRGB.g, 10) + ',' + parseInt(progressColorRGB.b, 10) + ')'; // Circle wrapper var $wrapper = $('<div/>', { 'class': "joubel-progress-circle-wrapper" }); //Active border indicates progress var $activeBorder = $('<div/>', { 'class': "joubel-progress-circle-active-border" }).appendTo($wrapper); //Background circle var $backgroundCircle = $('<div/>', { 'class': "joubel-progress-circle-circle" }).appendTo($activeBorder); //Progress text/number $('<span/>', { 'text': number + '%', 'class': "joubel-progress-circle-percentage" }).appendTo($backgroundCircle); var deg = number * 3.6; if (deg <= 180) { $activeBorder.css('background-image', 'linear-gradient(' + (90 + deg) + 'deg, transparent 50%, ' + fillColor + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } else { $activeBorder.css('background-image', 'linear-gradient(' + (deg - 90) + 'deg, transparent 50%, ' + progressColorString + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } this.$activeBorder = $activeBorder; this.$backgroundCircle = $backgroundCircle; this.$wrapper = $wrapper; this.initResizeFunctionality(); return $wrapper; } /** * Initializes resize functionality for the progress circle */ ProgressCircle.prototype.initResizeFunctionality = function () { var self = this; $(window).resize(function () { // Queue resize setTimeout(function () { self.resize(); }); }); // First resize setTimeout(function () { self.resize(); }, 0); }; /** * Resize function makes progress circle grow or shrink relative to parent container */ ProgressCircle.prototype.resize = function () { var $parent = this.$wrapper.parent(); if ($parent !== undefined && $parent) { // Measurements var fontSize = parseInt($parent.css('font-size'), 10); // Static sizes var fontSizeMultiplum = 3.75; var progressCircleWidthPx = parseInt((fontSize / 4.5), 10) % 2 === 0 ? parseInt((fontSize / 4.5), 10) + 4 : parseInt((fontSize / 4.5), 10) + 5; var progressCircleOffset = progressCircleWidthPx / 2; var width = fontSize * fontSizeMultiplum; var height = fontSize * fontSizeMultiplum; this.$activeBorder.css({ 'width': width, 'height': height }); this.$backgroundCircle.css({ 'width': width - progressCircleWidthPx, 'height': height - progressCircleWidthPx, 'top': progressCircleOffset, 'left': progressCircleOffset }); } }; /** * Hex to RGB conversion * @param hex * @returns {{r: Number, g: Number, b: Number}} */ ProgressCircle.prototype.hexToRgb = function (hex) { var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }; return ProgressCircle; }(H5P.jQuery)); ; var H5P = H5P || {}; H5P.SimpleRoundedButton = (function ($) { /** * Creates a new tip */ function SimpleRoundedButton(text) { var $simpleRoundedButton = $('<div>', { 'class': 'joubel-simple-rounded-button', 'title': text, 'role': 'button', 'tabindex': '0' }).keydown(function (e) { // 32 - space, 13 - enter if ([32, 13].indexOf(e.which) !== -1) { $(this).click(); e.preventDefault(); } }); $('<span>', { 'class': 'joubel-simple-rounded-button-text', 'html': text }).appendTo($simpleRoundedButton); return $simpleRoundedButton; } return SimpleRoundedButton; }(H5P.jQuery)); ; var H5P = H5P || {}; /** * Class responsible for creating speech bubbles */ H5P.JoubelSpeechBubble = (function ($) { var $currentSpeechBubble; var $currentContainer; var $tail; var $innerTail; var removeSpeechBubbleTimeout; var currentMaxWidth; var DEFAULT_MAX_WIDTH = 400; var iDevice = navigator.userAgent.match(/iPod|iPhone|iPad/g) ? true : false; /** * Creates a new speech bubble * * @param {H5P.jQuery} $container The speaking object * @param {string} text The text to display * @param {number} maxWidth The maximum width of the bubble * @return {H5P.JoubelSpeechBubble} */ function JoubelSpeechBubble($container, text, maxWidth) { maxWidth = maxWidth || DEFAULT_MAX_WIDTH; currentMaxWidth = maxWidth; $currentContainer = $container; this.isCurrent = function ($tip) { return $tip.is($currentContainer); }; this.remove = function () { remove(); }; var fadeOutSpeechBubble = function ($speechBubble) { if (!$speechBubble) { return; } // Stop removing bubble clearTimeout(removeSpeechBubbleTimeout); $speechBubble.removeClass('show'); setTimeout(function () { if ($speechBubble) { $speechBubble.remove(); $speechBubble = undefined; } }, 500); }; if ($currentSpeechBubble !== undefined) { remove(); } var $h5pContainer = getH5PContainer($container); // Make sure we fade out old speech bubble fadeOutSpeechBubble($currentSpeechBubble); // Create bubble $tail = $('<div class="joubel-speech-bubble-tail"></div>'); $innerTail = $('<div class="joubel-speech-bubble-inner-tail"></div>'); var $innerBubble = $( '<div class="joubel-speech-bubble-inner">' + '<div class="joubel-speech-bubble-text">' + text + '</div>' + '</div>' ).prepend($innerTail); $currentSpeechBubble = $( '<div class="joubel-speech-bubble" aria-live="assertive">' ).append([$tail, $innerBubble]) .appendTo($h5pContainer); // Show speech bubble with transition setTimeout(function () { $currentSpeechBubble.addClass('show'); }, 0); position($currentSpeechBubble, $currentContainer, maxWidth, $tail, $innerTail); // Handle click to close H5P.$body.on('mousedown.speechBubble', handleOutsideClick); // Handle window resizing H5P.$window.on('resize', '', handleResize); // Handle clicks when inside IV which blocks bubbling. $container.parents('.h5p-dialog') .on('mousedown.speechBubble', handleOutsideClick); if (iDevice) { H5P.$body.css('cursor', 'pointer'); } return this; } // Remove speechbubble if it belongs to a dom element that is about to be hidden H5P.externalDispatcher.on('domHidden', function (event) { if ($currentSpeechBubble !== undefined && event.data.$dom.find($currentContainer).length !== 0) { remove(); } }); /** * Returns the closest h5p container for the given DOM element. * * @param {object} $container jquery element * @return {object} the h5p container (jquery element) */ function getH5PContainer($container) { var $h5pContainer = $container.closest('.h5p-frame'); // Check closest h5p frame first, then check for container in case there is no frame. if (!$h5pContainer.length) { $h5pContainer = $container.closest('.h5p-container'); } return $h5pContainer; } /** * Event handler that is called when the window is resized. */ function handleResize() { position($currentSpeechBubble, $currentContainer, currentMaxWidth, $tail, $innerTail); } /** * Repositions the speech bubble according to the position of the container. * * @param {object} $currentSpeechbubble the speech bubble that should be positioned * @param {object} $container the container to which the speech bubble should point * @param {number} maxWidth the maximum width of the speech bubble * @param {object} $tail the tail (the triangle that points to the referenced container) * @param {object} $innerTail the inner tail (the triangle that points to the referenced container) */ function position($currentSpeechBubble, $container, maxWidth, $tail, $innerTail) { var $h5pContainer = getH5PContainer($container); // Calculate offset between the button and the h5p frame var offset = getOffsetBetween($h5pContainer, $container); var direction = (offset.bottom > offset.top ? 'bottom' : 'top'); var tipWidth = offset.outerWidth * 0.9; // Var needs to be renamed to make sense var bubbleWidth = tipWidth > maxWidth ? maxWidth : tipWidth; var bubblePosition = getBubblePosition(bubbleWidth, offset); var tailPosition = getTailPosition(bubbleWidth, bubblePosition, offset, $container.width()); // Need to set font-size, since element is appended to body. // Using same font-size as parent. In that way it will grow accordingly // when resizing var fontSize = 16;//parseFloat($parent.css('font-size')); // Set width and position of speech bubble $currentSpeechBubble.css(bubbleCSS( direction, bubbleWidth, bubblePosition, fontSize )); var preparedTailCSS = tailCSS(direction, tailPosition); $tail.css(preparedTailCSS); $innerTail.css(preparedTailCSS); } /** * Static function for removing the speechbubble */ var remove = function () { H5P.$body.off('mousedown.speechBubble'); H5P.$window.off('resize', '', handleResize); $currentContainer.parents('.h5p-dialog').off('mousedown.speechBubble'); if (iDevice) { H5P.$body.css('cursor', ''); } if ($currentSpeechBubble !== undefined) { // Apply transition, then remove speech bubble $currentSpeechBubble.removeClass('show'); // Make sure we remove any old timeout before reassignment clearTimeout(removeSpeechBubbleTimeout); removeSpeechBubbleTimeout = setTimeout(function () { $currentSpeechBubble.remove(); $currentSpeechBubble = undefined; }, 500); } // Don't return false here. If the user e.g. clicks a button when the bubble is visible, // we want the bubble to disapear AND the button to receive the event }; /** * Remove the speech bubble and container reference */ function handleOutsideClick(event) { if (event.target === $currentContainer[0]) { return; // Button clicks are not outside clicks } remove(); // There is no current container when a container isn't clicked $currentContainer = undefined; } /** * Calculate position for speech bubble * * @param {number} bubbleWidth The width of the speech bubble * @param {object} offset * @return {object} Return position for the speech bubble */ function getBubblePosition(bubbleWidth, offset) { var bubblePosition = {}; var tailOffset = 9; var widthOffset = bubbleWidth / 2; // Calculate top position bubblePosition.top = offset.top + offset.innerHeight; // Calculate bottom position bubblePosition.bottom = offset.bottom + offset.innerHeight + tailOffset; // Calculate left position if (offset.left < widthOffset) { bubblePosition.left = 3; } else if ((offset.left + widthOffset) > offset.outerWidth) { bubblePosition.left = offset.outerWidth - bubbleWidth - 3; } else { bubblePosition.left = offset.left - widthOffset + (offset.innerWidth / 2); } return bubblePosition; } /** * Calculate position for speech bubble tail * * @param {number} bubbleWidth The width of the speech bubble * @param {object} bubblePosition Speech bubble position * @param {object} offset * @param {number} iconWidth The width of the tip icon * @return {object} Return position for the tail */ function getTailPosition(bubbleWidth, bubblePosition, offset, iconWidth) { var tailPosition = {}; // Magic numbers. Tuned by hand so that the tail fits visually within // the bounds of the speech bubble. var leftBoundary = 9; var rightBoundary = bubbleWidth - 20; tailPosition.left = offset.left - bubblePosition.left + (iconWidth / 2) - 6; if (tailPosition.left < leftBoundary) { tailPosition.left = leftBoundary; } if (tailPosition.left > rightBoundary) { tailPosition.left = rightBoundary; } tailPosition.top = -6; tailPosition.bottom = -6; return tailPosition; } /** * Return bubble CSS for the desired growth direction * * @param {string} direction The direction the speech bubble will grow * @param {number} width The width of the speech bubble * @param {object} position Speech bubble position * @param {number} fontSize The size of the bubbles font * @return {object} Return CSS */ function bubbleCSS(direction, width, position, fontSize) { if (direction === 'top') { return { width: width + 'px', bottom: position.bottom + 'px', left: position.left + 'px', fontSize: fontSize + 'px', top: '' }; } else { return { width: width + 'px', top: position.top + 'px', left: position.left + 'px', fontSize: fontSize + 'px', bottom: '' }; } } /** * Return tail CSS for the desired growth direction * * @param {string} direction The direction the speech bubble will grow * @param {object} position Tail position * @return {object} Return CSS */ function tailCSS(direction, position) { if (direction === 'top') { return { bottom: position.bottom + 'px', left: position.left + 'px', top: '' }; } else { return { top: position.top + 'px', left: position.left + 'px', bottom: '' }; } } /** * Calculates the offset between an element inside a container and the * container. Only works if all the edges of the inner element are inside the * outer element. * Width/height of the elements is included as a convenience. * * @param {H5P.jQuery} $outer * @param {H5P.jQuery} $inner * @return {object} Position offset */ function getOffsetBetween($outer, $inner) { var outer = $outer[0].getBoundingClientRect(); var inner = $inner[0].getBoundingClientRect(); return { top: inner.top - outer.top, right: outer.right - inner.right, bottom: outer.bottom - inner.bottom, left: inner.left - outer.left, innerWidth: inner.width, innerHeight: inner.height, outerWidth: outer.width, outerHeight: outer.height }; } return JoubelSpeechBubble; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelThrobber = (function ($) { /** * Creates a new tip */ function JoubelThrobber() { // h5p-throbber css is described in core var $throbber = $('<div/>', { 'class': 'h5p-throbber' }); return $throbber; } return JoubelThrobber; }(H5P.jQuery)); ; H5P.JoubelTip = (function ($) { var $conv = $('<div/>'); /** * Creates a new tip element. * * NOTE that this may look like a class but it doesn't behave like one. * It returns a jQuery object. * * @param {string} tipHtml The text to display in the popup * @param {Object} [behaviour] Options * @param {string} [behaviour.tipLabel] Set to use a custom label for the tip button (you want this for good A11Y) * @param {boolean} [behaviour.helpIcon] Set to 'true' to Add help-icon classname to Tip button (changes the icon) * @param {boolean} [behaviour.showSpeechBubble] Set to 'false' to disable functionality (you may this in the editor) * @param {boolean} [behaviour.tabcontrol] Set to 'true' if you plan on controlling the tabindex in the parent (tabindex="-1") * @return {H5P.jQuery|undefined} Tip button jQuery element or 'undefined' if invalid tip */ function JoubelTip(tipHtml, behaviour) { // Keep track of the popup that appears when you click the Tip button var speechBubble; // Parse tip html to determine text var tipText = $conv.html(tipHtml).text().trim(); if (tipText === '') { return; // The tip has no textual content, i.e. it's invalid. } // Set default behaviour behaviour = $.extend({ tipLabel: tipText, helpIcon: false, showSpeechBubble: true, tabcontrol: false }, behaviour); // Create Tip button var $tipButton = $('<div/>', { class: 'joubel-tip-container' + (behaviour.showSpeechBubble ? '' : ' be-quiet'), 'aria-label': behaviour.tipLabel, 'aria-expanded': false, role: 'button', tabindex: (behaviour.tabcontrol ? -1 : 0), click: function (event) { // Toggle show/hide popup toggleSpeechBubble(); event.preventDefault(); }, keydown: function (event) { if (event.which === 32 || event.which === 13) { // Space & enter key // Toggle show/hide popup toggleSpeechBubble(); event.stopPropagation(); event.preventDefault(); } else { // Any other key // Toggle hide popup toggleSpeechBubble(false); } }, // Add markup to render icon html: '<span class="joubel-icon-tip-normal ' + (behaviour.helpIcon ? ' help-icon': '') + '">' + '<span class="h5p-icon-shadow"></span>' + '<span class="h5p-icon-speech-bubble"></span>' + '<span class="h5p-icon-info"></span>' + '</span>' // IMPORTANT: All of the markup elements must have 'pointer-events: none;' }); const $tipAnnouncer = $('<div>', { 'class': 'hidden-but-read', 'aria-live': 'polite', appendTo: $tipButton, }); /** * Tip button interaction handler. * Toggle show or hide the speech bubble popup when interacting with the * Tip button. * * @private * @param {boolean} [force] 'true' shows and 'false' hides. */ var toggleSpeechBubble = function (force) { if (speechBubble !== undefined && speechBubble.isCurrent($tipButton)) { // Hide current popup speechBubble.remove(); speechBubble = undefined; $tipButton.attr('aria-expanded', false); $tipAnnouncer.html(''); } else if (force !== false && behaviour.showSpeechBubble) { // Create and show new popup speechBubble = H5P.JoubelSpeechBubble($tipButton, tipHtml); $tipButton.attr('aria-expanded', true); $tipAnnouncer.html(tipHtml); } }; return $tipButton; } return JoubelTip; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelSlider = (function ($) { /** * Creates a new Slider * * @param {object} [params] Additional parameters */ function JoubelSlider(params) { H5P.EventDispatcher.call(this); this.$slider = $('<div>', $.extend({ 'class': 'h5p-joubel-ui-slider' }, params)); this.$slides = []; this.currentIndex = 0; this.numSlides = 0; } JoubelSlider.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelSlider.prototype.constructor = JoubelSlider; JoubelSlider.prototype.addSlide = function ($content) { $content.addClass('h5p-joubel-ui-slide').css({ 'left': (this.numSlides*100) + '%' }); this.$slider.append($content); this.$slides.push($content); this.numSlides++; if(this.numSlides === 1) { $content.addClass('current'); } }; JoubelSlider.prototype.attach = function ($container) { $container.append(this.$slider); }; JoubelSlider.prototype.move = function (index) { var self = this; if(index === 0) { self.trigger('first-slide'); } if(index+1 === self.numSlides) { self.trigger('last-slide'); } self.trigger('move'); var $previousSlide = self.$slides[this.currentIndex]; H5P.Transition.onTransitionEnd(this.$slider, function () { $previousSlide.removeClass('current'); self.trigger('moved'); }); this.$slides[index].addClass('current'); var translateX = 'translateX(' + (-index*100) + '%)'; this.$slider.css({ '-webkit-transform': translateX, '-moz-transform': translateX, '-ms-transform': translateX, 'transform': translateX }); this.currentIndex = index; }; JoubelSlider.prototype.remove = function () { this.$slider.remove(); }; JoubelSlider.prototype.next = function () { if(this.currentIndex+1 >= this.numSlides) { return; } this.move(this.currentIndex+1); }; JoubelSlider.prototype.previous = function () { this.move(this.currentIndex-1); }; JoubelSlider.prototype.first = function () { this.move(0); }; JoubelSlider.prototype.last = function () { this.move(this.numSlides-1); }; return JoubelSlider; })(H5P.jQuery); ; var H5P = H5P || {}; /** * @module */ H5P.JoubelScoreBar = (function ($) { /* Need to use an id for the star SVG since that is the only way to reference SVG filters */ var idCounter = 0; /** * Creates a score bar * @class H5P.JoubelScoreBar * @param {number} maxScore Maximum score * @param {string} [label] Makes it easier for readspeakers to identify the scorebar * @param {string} [helpText] Score explanation * @param {string} [scoreExplanationButtonLabel] Label for score explanation button */ function JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel) { var self = this; self.maxScore = maxScore; self.score = 0; idCounter++; /** * @const {string} */ self.STAR_MARKUP = '<svg tabindex="-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 63.77 53.87" aria-hidden="true" focusable="false">' + '<title>star</title>' + '<filter tabindex="-1" id="h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + '" x0="-50%" y0="-50%" width="200%" height="200%">' + '<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"></feGaussianBlur>' + '<feOffset dy="2" dx="4"></feOffset>' + '<feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' + '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' + '<feComposite in2="shadowDiff" operator="in"></feComposite>' + '<feComposite in2="SourceGraphic" operator="over" result="firstfilter"></feComposite>' + '<feGaussianBlur in="firstfilter" stdDeviation="3" result="blur2"></feGaussianBlur>' + '<feOffset dy="-2" dx="-4"></feOffset>' + '<feComposite in2="firstfilter" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' + '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' + '<feComposite in2="shadowDiff" operator="in"></feComposite>' + '<feComposite in2="firstfilter" operator="over"></feComposite>' + '</filter>' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-shadow" d="M35.08,43.41V9.16H20.91v0L9.51,10.85,9,10.93C2.8,12.18,0,17,0,21.25a11.22,11.22,0,0,0,3,7.48l8.73,8.53-1.07,6.16Z"/>' + '<g tabindex="-1">' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-border" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '<path tabindex="-1" class="h5p-joubelui-score-bar-star-fill" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '<path tabindex="-1" filter="url(#h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + ')" class="h5p-joubelui-score-bar-star-fill-full-score" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' + '</g>' + '</svg>'; /** * @function appendTo * @memberOf H5P.JoubelScoreBar# * @param {H5P.jQuery} $wrapper Dom container */ self.appendTo = function ($wrapper) { self.$scoreBar.appendTo($wrapper); }; /** * Create the text representation of the scorebar . * * @private * @return {string} */ var createLabel = function (score) { if (!label) { return ''; } return label.replace(':num', score).replace(':total', self.maxScore); }; /** * Creates the html for this widget * * @method createHtml * @private */ var createHtml = function () { // Container div self.$scoreBar = $('<div>', { 'class': 'h5p-joubelui-score-bar', }); var $visuals = $('<div>', { 'class': 'h5p-joubelui-score-bar-visuals', appendTo: self.$scoreBar }); // The progress bar wrapper self.$progressWrapper = $('<div>', { 'class': 'h5p-joubelui-score-bar-progress-wrapper', appendTo: $visuals }); self.$progress = $('<div>', { 'class': 'h5p-joubelui-score-bar-progress', 'html': createLabel(self.score), appendTo: self.$progressWrapper }); // The star $('<div>', { 'class': 'h5p-joubelui-score-bar-star', html: self.STAR_MARKUP }).appendTo($visuals); // The score container var $numerics = $('<div>', { 'class': 'h5p-joubelui-score-numeric', appendTo: self.$scoreBar, 'aria-hidden': true }); // The current score self.$scoreCounter = $('<span>', { 'class': 'h5p-joubelui-score-number h5p-joubelui-score-number-counter', text: 0, appendTo: $numerics }); // The separator $('<span>', { 'class': 'h5p-joubelui-score-number-separator', text: '/', appendTo: $numerics }); // Max score self.$maxScore = $('<span>', { 'class': 'h5p-joubelui-score-number h5p-joubelui-score-max', text: self.maxScore, appendTo: $numerics }); if (helpText) { H5P.JoubelUI.createTip(helpText, { tipLabel: scoreExplanationButtonLabel ? scoreExplanationButtonLabel : helpText, helpIcon: true }).appendTo(self.$scoreBar); self.$scoreBar.addClass('h5p-score-bar-has-help'); } }; /** * Set the current score * @method setScore * @memberOf H5P.JoubelScoreBar# * @param {number} score */ self.setScore = function (score) { // Do nothing if score hasn't changed if (score === self.score) { return; } self.score = score > self.maxScore ? self.maxScore : score; self.updateVisuals(); }; /** * Increment score * @method incrementScore * @memberOf H5P.JoubelScoreBar# * @param {number=} incrementBy Optional parameter, defaults to 1 */ self.incrementScore = function (incrementBy) { self.setScore(self.score + (incrementBy || 1)); }; /** * Set the max score * @method setMaxScore * @memberOf H5P.JoubelScoreBar# * @param {number} maxScore The max score */ self.setMaxScore = function (maxScore) { self.maxScore = maxScore; }; /** * Updates the progressbar visuals * @memberOf H5P.JoubelScoreBar# * @method updateVisuals */ self.updateVisuals = function () { self.$progress.html(createLabel(self.score)); self.$scoreCounter.text(self.score); self.$maxScore.text(self.maxScore); setTimeout(function () { // Start the progressbar animation self.$progress.css({ width: ((self.score / self.maxScore) * 100) + '%' }); H5P.Transition.onTransitionEnd(self.$progress, function () { // If fullscore fill the star and start the animation self.$scoreBar.toggleClass('h5p-joubelui-score-bar-full-score', self.score === self.maxScore); self.$scoreBar.toggleClass('h5p-joubelui-score-bar-animation-active', self.score === self.maxScore); // Only allow the star animation to run once self.$scoreBar.one("animationend", function() { self.$scoreBar.removeClass("h5p-joubelui-score-bar-animation-active"); }); }, 600); }, 300); }; /** * Removes all classes * @method reset */ self.reset = function () { self.$scoreBar.removeClass('h5p-joubelui-score-bar-full-score'); }; createHtml(); } return JoubelScoreBar; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelProgressbar = (function ($) { /** * Joubel progressbar class * @method JoubelProgressbar * @constructor * @param {number} steps Number of steps * @param {Object} [options] Additional options * @param {boolean} [options.disableAria] Disable readspeaker assistance * @param {string} [options.progressText] A progress text for describing * current progress out of total progress for readspeakers. * e.g. "Slide :num of :total" */ function JoubelProgressbar(steps, options) { H5P.EventDispatcher.call(this); var self = this; this.options = $.extend({ progressText: 'Slide :num of :total' }, options); this.currentStep = 0; this.steps = steps; this.$progressbar = $('<div>', { 'class': 'h5p-joubelui-progressbar' }); this.$background = $('<div>', { '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 = $('<div>', { 'class': 'h5p-joubelui-progressbar-slide-status-text', 'aria-live': 'assertive' }).appendTo(this.$progressbar); } var interpolatedProgressText = self.options.progressText .replace(':num', self.currentStep) .replace(':total', self.steps); this.$currentStatus.html(interpolatedProgressText); }; /** * 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); ; H5P.Question = (function ($, EventDispatcher, JoubelUI) { /** * Extending this class make it alot easier to create tasks for other * content types. * * @class H5P.Question * @extends H5P.EventDispatcher * @param {string} type */ function Question(type) { var self = this; // Inheritance EventDispatcher.call(self); // Register default section order self.order = ['video', 'image', 'introduction', 'content', 'explanation', 'feedback', 'scorebar', 'buttons', 'read']; // Keep track of registered sections var sections = {}; // Buttons var buttons = {}; var buttonOrder = []; // Wrapper when attached var $wrapper; // Click element var clickElement; // ScoreBar var scoreBar; // Keep track of the feedback's visual status. var showFeedback; // Keep track of which buttons are scheduled for hiding. var buttonsToHide = []; // Keep track of which buttons are scheduled for showing. var buttonsToShow = []; // Keep track of the hiding and showing of buttons. var toggleButtonsTimer; var toggleButtonsTransitionTimer; var buttonTruncationTimer; // Keeps track of initialization of question var initialized = false; /** * @type {Object} behaviour Behaviour of Question * @property {Boolean} behaviour.disableFeedback Set to true to disable feedback section */ var behaviour = { disableFeedback: false, disableReadSpeaker: false }; // Keeps track of thumb state var imageThumb = true; // Keeps track of image transitions var imageTransitionTimer; // Keep track of whether sections is transitioning. var sectionsIsTransitioning = false; // Keep track of auto play state var disableAutoPlay = false; // Feedback transition timer var feedbackTransitionTimer; // Used when reading messages to the user var $read, readText; /** * Register section with given content. * * @private * @param {string} section ID of the section * @param {(string|H5P.jQuery)} [content] */ var register = function (section, content) { sections[section] = {}; var $e = sections[section].$element = $('<div/>', { 'class': 'h5p-question-' + section, }); if (content) { $e[content instanceof $ ? 'append' : 'html'](content); } }; /** * Update registered section with content. * * @private * @param {string} section ID of the section * @param {(string|H5P.jQuery)} content */ var update = function (section, content) { if (content instanceof $) { sections[section].$element.html('').append(content); } else { sections[section].$element.html(content); } }; /** * Insert element with given ID into the DOM. * * @private * @param {array|Array|string[]} order * List with ordered element IDs * @param {string} id * ID of the element to be inserted * @param {Object} elements * Maps ID to the elements * @param {H5P.jQuery} $container * Parent container of the elements */ var insert = function (order, id, elements, $container) { // Try to find an element id should be after for (var i = 0; i < order.length; i++) { if (order[i] === id) { // Found our pos while (i > 0 && (elements[order[i - 1]] === undefined || !elements[order[i - 1]].isVisible)) { i--; } if (i === 0) { // We are on top. elements[id].$element.prependTo($container); } else { // Add after element elements[id].$element.insertAfter(elements[order[i - 1]].$element); } elements[id].isVisible = true; break; } } }; /** * Make feedback into a popup and position relative to click. * * @private * @param {string} [closeText] Text for the close button */ var makeFeedbackPopup = function (closeText) { var $element = sections.feedback.$element; var $parent = sections.content.$element; var $click = (clickElement != null ? clickElement.$element : null); $element.appendTo($parent).addClass('h5p-question-popup'); if (sections.scorebar) { sections.scorebar.$element.appendTo($element); } $parent.addClass('h5p-has-question-popup'); // Draw the tail var $tail = $('<div/>', { 'class': 'h5p-question-feedback-tail' }).hide() .appendTo($parent); // Draw the close button var $close = $('<div/>', { 'class': 'h5p-question-feedback-close', 'tabindex': 0, 'title': closeText, on: { click: function (event) { $element.remove(); $tail.remove(); event.preventDefault(); }, keydown: function (event) { switch (event.which) { case 13: // Enter case 32: // Space $element.remove(); $tail.remove(); event.preventDefault(); } } } }).hide().appendTo($element); if ($click != null) { if ($click.hasClass('correct')) { $element.addClass('h5p-question-feedback-correct'); $close.show(); sections.buttons.$element.hide(); } else { sections.buttons.$element.appendTo(sections.feedback.$element); } } positionFeedbackPopup($element, $click); }; /** * Position the feedback popup. * * @private * @param {H5P.jQuery} $element Feedback div * @param {H5P.jQuery} $click Visual click div */ var positionFeedbackPopup = function ($element, $click) { var $container = $element.parent(); var $tail = $element.siblings('.h5p-question-feedback-tail'); var popupWidth = $element.outerWidth(); var popupHeight = setElementHeight($element); var space = 15; var disableTail = false; var positionY = $container.height() / 2 - popupHeight / 2; var positionX = $container.width() / 2 - popupWidth / 2; var tailX = 0; var tailY = 0; var tailRotation = 0; if ($click != null) { // Edge detection for click, takes space into account var clickNearTop = ($click[0].offsetTop < space); var clickNearBottom = ($click[0].offsetTop + $click.height() > $container.height() - space); var clickNearLeft = ($click[0].offsetLeft < space); var clickNearRight = ($click[0].offsetLeft + $click.width() > $container.width() - space); // Click is not in a corner or close to edge, calculate position normally positionX = $click[0].offsetLeft - popupWidth / 2 + $click.width() / 2; positionY = $click[0].offsetTop - popupHeight - space; tailX = positionX + popupWidth / 2 - $tail.width() / 2; tailY = positionY + popupHeight - ($tail.height() / 2); tailRotation = 225; // If popup is outside top edge, position under click instead if (popupHeight + space > $click[0].offsetTop) { positionY = $click[0].offsetTop + $click.height() + space; tailY = positionY - $tail.height() / 2 ; tailRotation = 45; } // If popup is outside left edge, position left if (positionX < 0) { positionX = 0; } // If popup is outside right edge, position right if (positionX + popupWidth > $container.width()) { positionX = $container.width() - popupWidth; } // Special cases such as corner clicks, or close to an edge, they override X and Y positions if met if (clickNearTop && (clickNearLeft || clickNearRight)) { positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth); positionY = $click[0].offsetTop + $click.height(); disableTail = true; } else if (clickNearBottom && (clickNearLeft || clickNearRight)) { positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth); positionY = $click[0].offsetTop - popupHeight; disableTail = true; } else if (!clickNearTop && !clickNearBottom) { if (clickNearLeft || clickNearRight) { positionY = $click[0].offsetTop - popupHeight / 2 + $click.width() / 2; positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() + space : -popupWidth + -space); // Make sure this does not position the popup off screen if (positionX < 0) { positionX = 0; disableTail = true; } else { tailX = positionX + (clickNearLeft ? - $tail.width() / 2 : popupWidth - $tail.width() / 2); tailY = positionY + popupHeight / 2 - $tail.height() / 2; tailRotation = (clickNearLeft ? 315 : 135); } } } // Contain popup from overflowing bottom edge if (positionY + popupHeight > $container.height()) { positionY = $container.height() - popupHeight; if (popupHeight > $container.height() - ($click[0].offsetTop + $click.height() + space)) { disableTail = true; } } } else { disableTail = true; } // Contain popup from ovreflowing top edge if (positionY < 0) { positionY = 0; } $element.css({top: positionY, left: positionX}); $tail.css({top: tailY, left: tailX}); if (!disableTail) { $tail.css({ 'left': tailX, 'top': tailY, 'transform': 'rotate(' + tailRotation + 'deg)' }).show(); } else { $tail.hide(); } }; /** * Set element max height, used for animations. * * @param {H5P.jQuery} $element */ var setElementHeight = function ($element) { if (!$element.is(':visible')) { // No animation $element.css('max-height', 'none'); return; } // If this element is shown in the popup, we can't set width to 100%, // since it already has a width set in CSS var isFeedbackPopup = $element.hasClass('h5p-question-popup'); // Get natural element height var $tmp = $element.clone() .css({ 'position': 'absolute', 'max-height': 'none', 'width': isFeedbackPopup ? '' : '100%' }) .appendTo($element.parent()); // Need to take margins into account when calculating available space var sideMargins = parseFloat($element.css('margin-left')) + parseFloat($element.css('margin-right')); var tmpElWidth = $tmp.css('width') ? $tmp.css('width') : '100%'; $tmp.css('width', 'calc(' + tmpElWidth + ' - ' + sideMargins + 'px)'); // Apply height to element var h = Math.round($tmp.get(0).getBoundingClientRect().height); var fontSize = parseFloat($element.css('fontSize')); var relativeH = h / fontSize; $element.css('max-height', relativeH + 'em'); $tmp.remove(); if (h > 0 && sections.buttons && sections.buttons.$element === $element) { // Make sure buttons section is visible showSection(sections.buttons); // Resize buttons after resizing button section setTimeout(resizeButtons, 150); } return h; }; /** * Does the actual job of hiding the buttons scheduled for hiding. * * @private * @param {boolean} [relocateFocus] Find a new button to focus */ var hideButtons = function (relocateFocus) { for (var i = 0; i < buttonsToHide.length; i++) { hideButton(buttonsToHide[i].id); } buttonsToHide = []; if (relocateFocus) { self.focusButton(); } }; /** * Does the actual hiding. * @private * @param {string} buttonId */ var hideButton = function (buttonId) { // Using detach() vs hide() makes it harder to cheat. buttons[buttonId].$element.detach(); buttons[buttonId].isVisible = false; }; /** * Shows the buttons on the next tick. This is to avoid buttons flickering * If they're both added and removed on the same tick. * * @private */ var toggleButtons = function () { // If no buttons section, return if (sections.buttons === undefined) { return; } // Clear transition timer, reevaluate if buttons will be detached clearTimeout(toggleButtonsTransitionTimer); // Show buttons for (var i = 0; i < buttonsToShow.length; i++) { insert(buttonOrder, buttonsToShow[i].id, buttons, sections.buttons.$element); buttons[buttonsToShow[i].id].isVisible = true; } buttonsToShow = []; // Hide buttons var numToHide = 0; var relocateFocus = false; for (var j = 0; j < buttonsToHide.length; j++) { var button = buttons[buttonsToHide[j].id]; if (button.isVisible) { numToHide += 1; } if (button.$element.is(':focus')) { // Move focus to the first visible button. relocateFocus = true; } } var animationTimer = 150; if (sections.feedback && sections.feedback.$element.hasClass('h5p-question-popup')) { animationTimer = 0; } if (numToHide === sections.buttons.$element.children().length) { // All buttons are going to be hidden. Hide container using transition. hideSection(sections.buttons); // Detach buttons hideButtons(relocateFocus); } else { hideButtons(relocateFocus); // Show button section if (!sections.buttons.$element.is(':empty')) { showSection(sections.buttons); setElementHeight(sections.buttons.$element); // Trigger resize after animation toggleButtonsTransitionTimer = setTimeout(function () { self.trigger('resize'); }, animationTimer); } // Resize buttons to fit container resizeButtons(); } toggleButtonsTimer = undefined; }; /** * Allows for scaling of the question image. */ var scaleImage = function () { var $imgSection = sections.image.$element; clearTimeout(imageTransitionTimer); // Add this here to avoid initial transition of the image making // content overflow. Alternatively we need to trigger a resize. $imgSection.addClass('animatable'); if (imageThumb) { // Expand image $(this).attr('aria-expanded', true); $imgSection.addClass('h5p-question-image-fill-width'); imageThumb = false; imageTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 600); } else { // Scale down image $(this).attr('aria-expanded', false); $imgSection.removeClass('h5p-question-image-fill-width'); imageThumb = true; imageTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 600); } }; /** * Get scrollable ancestor of element * * @private * @param {H5P.jQuery} $element * @param {Number} [currDepth=0] Current recursive calls to ancestor, stop at maxDepth * @param {Number} [maxDepth=5] Maximum depth for finding ancestor. * @returns {H5P.jQuery} Parent element that is scrollable */ var findScrollableAncestor = function ($element, currDepth, maxDepth) { if (!currDepth) { currDepth = 0; } if (!maxDepth) { maxDepth = 5; } // Check validation of element or if we have reached document root if (!$element || !($element instanceof $) || document === $element.get(0) || currDepth >= maxDepth) { return; } if ($element.css('overflow-y') === 'auto') { return $element; } else { return findScrollableAncestor($element.parent(), currDepth + 1, maxDepth); } }; /** * Scroll to bottom of Question. * * @private */ var scrollToBottom = function () { if (!$wrapper || ($wrapper.hasClass('h5p-standalone') && !H5P.isFullscreen)) { return; // No scroll } var scrollableAncestor = findScrollableAncestor($wrapper); // Scroll to bottom of scrollable ancestor if (scrollableAncestor) { scrollableAncestor.animate({ scrollTop: $wrapper.css('height') }, "slow"); } }; /** * Resize buttons to fit container width * * @private */ var resizeButtons = function () { if (!buttons || !sections.buttons) { return; } var go = function () { // Don't do anything if button elements are not visible yet if (!sections.buttons.$element.is(':visible')) { return; } // Width of all buttons var buttonsWidth = { max: 0, min: 0, current: 0 }; for (var i in buttons) { var button = buttons[i]; if (button.isVisible) { setButtonWidth(buttons[i]); buttonsWidth.max += button.width.max; buttonsWidth.min += button.width.min; buttonsWidth.current += button.isTruncated ? button.width.min : button.width.max; } } var makeButtonsFit = function (availableWidth) { if (buttonsWidth.max < availableWidth) { // It is room for everyone on the right side of the score bar (without truncating) if (buttonsWidth.max !== buttonsWidth.current) { // Need to make everyone big restoreButtonLabels(buttonsWidth.current, availableWidth); } return true; } else if (buttonsWidth.min < availableWidth) { // Is it room for everyone on the right side of the score bar with truncating? if (buttonsWidth.current > availableWidth) { removeButtonLabels(buttonsWidth.current, availableWidth); } else { restoreButtonLabels(buttonsWidth.current, availableWidth); } return true; } return false; }; toggleFullWidthScorebar(false); var buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1; if (!makeButtonsFit(buttonSectionWidth)) { // If we get here we need to wrap: toggleFullWidthScorebar(true); buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1; makeButtonsFit(buttonSectionWidth); } }; // If visible, resize right away if (sections.buttons.$element.is(':visible')) { go(); } else { // If not visible, try on the next tick // Clear button truncation timer if within a button truncation function if (buttonTruncationTimer) { clearTimeout(buttonTruncationTimer); } buttonTruncationTimer = setTimeout(function () { buttonTruncationTimer = undefined; go(); }, 0); } }; var toggleFullWidthScorebar = function (enabled) { if (sections.scorebar && sections.scorebar.$element && sections.scorebar.$element.hasClass('h5p-question-visible')) { sections.buttons.$element.addClass('has-scorebar'); sections.buttons.$element.toggleClass('wrap', enabled); sections.scorebar.$element.toggleClass('full-width', enabled); } else { sections.buttons.$element.removeClass('has-scorebar'); } }; /** * Remove button labels until they use less than max width. * * @private * @param {Number} buttonsWidth Total width of all buttons * @param {Number} maxButtonsWidth Max width allowed for buttons */ var removeButtonLabels = function (buttonsWidth, maxButtonsWidth) { // Reverse traversal for (var i = buttonOrder.length - 1; i >= 0; i--) { var buttonId = buttonOrder[i]; var button = buttons[buttonId]; if (!button.isTruncated && button.isVisible) { var $button = button.$element; buttonsWidth -= button.width.max - button.width.min; // Remove label button.$element.attr('aria-label', $button.text()).html('').addClass('truncated'); button.isTruncated = true; if (buttonsWidth <= maxButtonsWidth) { // Buttons are small enough. return; } } } }; /** * Restore button labels until it fills maximum possible width without exceeding the max width. * * @private * @param {Number} buttonsWidth Total width of all buttons * @param {Number} maxButtonsWidth Max width allowed for buttons */ var restoreButtonLabels = function (buttonsWidth, maxButtonsWidth) { for (var i = 0; i < buttonOrder.length; i++) { var buttonId = buttonOrder[i]; var button = buttons[buttonId]; if (button.isTruncated && button.isVisible) { // Calculate new total width of buttons with a static pixel for consistency cross-browser buttonsWidth += button.width.max - button.width.min + 1; if (buttonsWidth > maxButtonsWidth) { return; } // Restore label button.$element.html(button.text); button.$element.removeClass('truncated'); button.isTruncated = false; } } }; /** * Helper function for finding index of keyValue in array * * @param {String} keyValue Value to be found * @param {String} key In key * @param {Array} array In array * @returns {number} */ var existsInArray = function (keyValue, key, array) { var i; for (i = 0; i < array.length; i++) { if (array[i][key] === keyValue) { return i; } } return -1; }; /** * Show a section * @param {Object} section */ var showSection = function (section) { section.$element.addClass('h5p-question-visible'); section.isVisible = true; }; /** * Hide a section * @param {Object} section */ var hideSection = function (section) { section.$element.css('max-height', ''); section.isVisible = false; setTimeout(function () { // Only hide if section hasn't been set to visible in the meantime if (!section.isVisible) { section.$element.removeClass('h5p-question-visible'); } }, 150); }; /** * Set behaviour for question. * * @param {Object} options An object containing behaviour that will be extended by Question */ self.setBehaviour = function (options) { $.extend(behaviour, options); }; /** * A video to display above the task. * * @param {object} params */ self.setVideo = function (params) { sections.video = { $element: $('<div/>', { 'class': 'h5p-question-video' }) }; if (disableAutoPlay && params.params.playback) { params.params.playback.autoplay = false; } // Never fit to wrapper if (!params.params.visuals) { params.params.visuals = {}; } params.params.visuals.fit = false; sections.video.instance = H5P.newRunnable(params, self.contentId, sections.video.$element, true); var fromVideo = false; // Hack to avoid never ending loop sections.video.instance.on('resize', function () { fromVideo = true; self.trigger('resize'); fromVideo = false; }); self.on('resize', function () { if (!fromVideo) { sections.video.instance.trigger('resize'); } }); return self; }; /** * Will stop any playback going on in the task. */ self.pause = function () { if (sections.video && sections.video.isVisible) { sections.video.instance.pause(); } }; /** * Start playback of video */ self.play = function () { if (sections.video && sections.video.isVisible) { sections.video.instance.play(); } }; /** * Disable auto play, useful in editors. */ self.disableAutoPlay = function () { disableAutoPlay = true; }; /** * Add task image. * * @param {string} path Relative * @param {Object} [options] Options object * @param {string} [options.alt] Text representation * @param {string} [options.title] Hover text * @param {Boolean} [options.disableImageZooming] Set as true to disable image zooming */ self.setImage = function (path, options) { options = options ? options : {}; sections.image = {}; // Image container sections.image.$element = $('<div/>', { 'class': 'h5p-question-image h5p-question-image-fill-width' }); // Inner wrap var $imgWrap = $('<div/>', { 'class': 'h5p-question-image-wrap', appendTo: sections.image.$element }); // Image element var $img = $('<img/>', { src: H5P.getPath(path, self.contentId), alt: (options.alt === undefined ? '' : options.alt), title: (options.title === undefined ? '' : options.title), on: { load: function () { self.trigger('imageLoaded', this); self.trigger('resize'); } }, appendTo: $imgWrap }); // Disable image zooming if (options.disableImageZooming) { $img.css('maxHeight', 'none'); // Make sure we are using the correct amount of width at all times var determineImgWidth = function () { // Remove margins if natural image width is bigger than section width var imageSectionWidth = sections.image.$element.get(0).getBoundingClientRect().width; // Do not transition, for instant measurements $imgWrap.css({ '-webkit-transition': 'none', 'transition': 'none' }); // Margin as translateX on both sides of image. var diffX = 2 * ($imgWrap.get(0).getBoundingClientRect().left - sections.image.$element.get(0).getBoundingClientRect().left); if ($img.get(0).naturalWidth >= imageSectionWidth - diffX) { sections.image.$element.addClass('h5p-question-image-fill-width'); } else { // Use margin for small res images sections.image.$element.removeClass('h5p-question-image-fill-width'); } // Reset transition rules $imgWrap.css({ '-webkit-transition': '', 'transition': '' }); }; // Determine image width if ($img.is(':visible')) { determineImgWidth(); } else { $img.on('load', determineImgWidth); } // Skip adding zoom functionality return; } var sizeDetermined = false; var determineSize = function () { if (sizeDetermined || !$img.is(':visible')) { return; // Try again next time. } $imgWrap.addClass('h5p-question-image-scalable') .attr('aria-expanded', false) .attr('role', 'button') .attr('tabIndex', '0') .on('click', function (event) { if (event.which === 1) { scaleImage.apply(this); // Left mouse button click } }).on('keypress', function (event) { if (event.which === 32) { event.preventDefault(); // Prevent default behaviour; page scroll down scaleImage.apply(this); // Space bar pressed } }); sections.image.$element.removeClass('h5p-question-image-fill-width'); sizeDetermined = true; // Prevent any futher events }; self.on('resize', determineSize); return self; }; /** * Add the introduction section. * * @param {(string|H5P.jQuery)} content */ self.setIntroduction = function (content) { register('introduction', content); return self; }; /** * Add the content section. * * @param {(string|H5P.jQuery)} content * @param {Object} [options] * @param {string} [options.class] */ self.setContent = function (content, options) { register('content', content); if (options && options.class) { sections.content.$element.addClass(options.class); } return self; }; /** * Force readspeaker to read text. Useful when you have to use * setTimeout for animations. */ self.read = function (content) { if (!$read) { return; // Not ready yet } if (readText) { // Combine texts if called multiple times readText += (readText.substr(-1, 1) === '.' ? ' ' : '. ') + content; } else { readText = content; } // Set text $read.html(readText); setTimeout(function () { // Stop combining when done reading readText = null; $read.html(''); }, 100); }; /** * Read feedback */ self.readFeedback = function () { var invalidFeedback = behaviour.disableReadSpeaker || !showFeedback || !sections.feedback || !sections.feedback.$element; if (invalidFeedback) { return; } var $feedbackText = $('.h5p-question-feedback-content-text', sections.feedback.$element); if ($feedbackText && $feedbackText.html() && $feedbackText.html().length) { self.read($feedbackText.html()); } }; /** * Remove feedback * * @return {H5P.Question} */ self.removeFeedback = function () { clearTimeout(feedbackTransitionTimer); if (sections.feedback && showFeedback) { showFeedback = false; // Hide feedback & scorebar hideSection(sections.scorebar); hideSection(sections.feedback); sectionsIsTransitioning = true; // Detach after transition feedbackTransitionTimer = setTimeout(function () { // Avoiding Transition.onTransitionEnd since it will register multiple events, and there's no way to cancel it if the transition changes back to "show" while the animation is happening. if (!showFeedback) { sections.feedback.$element.children().detach(); sections.scorebar.$element.children().detach(); // Trigger resize after animation self.trigger('resize'); } sectionsIsTransitioning = false; scoreBar.setScore(0); }, 150); if ($wrapper) { $wrapper.find('.h5p-question-feedback-tail').remove(); } } return self; }; /** * Set feedback message. * * @param {string} [content] * @param {number} score The score * @param {number} maxScore The maximum score for this question * @param {string} [scoreBarLabel] Makes it easier for readspeakers to identify the scorebar * @param {string} [helpText] Help text that describes the score inside a tip icon * @param {object} [popupSettings] Extra settings for popup feedback * @param {boolean} [popupSettings.showAsPopup] Should the feedback display as popup? * @param {string} [popupSettings.closeText] Translation for close button text * @param {object} [popupSettings.click] Element representing where user clicked on screen */ self.setFeedback = function (content, score, maxScore, scoreBarLabel, helpText, popupSettings, scoreExplanationButtonLabel) { // Feedback is disabled if (behaviour.disableFeedback) { return self; } // Need to toggle buttons right away to avoid flickering/blinking // Note: This means content types should invoke hide/showButton before setFeedback toggleButtons(); clickElement = (popupSettings != null && popupSettings.click != null ? popupSettings.click : null); clearTimeout(feedbackTransitionTimer); var $feedback = $('<div>', { 'class': 'h5p-question-feedback-container' }); var $feedbackContent = $('<div>', { 'class': 'h5p-question-feedback-content' }).appendTo($feedback); // Feedback text $('<div>', { 'class': 'h5p-question-feedback-content-text', 'html': content }).appendTo($feedbackContent); var $scorebar = $('<div>', { 'class': 'h5p-question-scorebar-container' }); if (scoreBar === undefined) { scoreBar = JoubelUI.createScoreBar(maxScore, scoreBarLabel, helpText, scoreExplanationButtonLabel); } scoreBar.appendTo($scorebar); $feedbackContent.toggleClass('has-content', content !== undefined && content.length > 0); // Feedback for readspeakers if (!behaviour.disableReadSpeaker && scoreBarLabel) { self.read(scoreBarLabel.replace(':num', score).replace(':total', maxScore) + '. ' + (content ? content : '')); } showFeedback = true; if (sections.feedback) { // Update section update('feedback', $feedback); update('scorebar', $scorebar); } else { // Create section register('feedback', $feedback); register('scorebar', $scorebar); if (initialized && $wrapper) { insert(self.order, 'feedback', sections, $wrapper); insert(self.order, 'scorebar', sections, $wrapper); } } showSection(sections.feedback); showSection(sections.scorebar); resizeButtons(); if (popupSettings != null && popupSettings.showAsPopup == true) { makeFeedbackPopup(popupSettings.closeText); scoreBar.setScore(score); } else { // Show feedback section feedbackTransitionTimer = setTimeout(function () { setElementHeight(sections.feedback.$element); setElementHeight(sections.scorebar.$element); sectionsIsTransitioning = true; // Scroll to bottom after showing feedback scrollToBottom(); // Trigger resize after animation feedbackTransitionTimer = setTimeout(function () { sectionsIsTransitioning = false; self.trigger('resize'); scoreBar.setScore(score); }, 150); }, 0); } return self; }; /** * Set feedback content (no animation). * * @param {string} content * @param {boolean} [extendContent] True will extend content, instead of replacing it */ self.updateFeedbackContent = function (content, extendContent) { if (sections.feedback && sections.feedback.$element) { if (extendContent) { content = $('.h5p-question-feedback-content', sections.feedback.$element).html() + ' ' + content; } // Update feedback content html $('.h5p-question-feedback-content', sections.feedback.$element).html(content).addClass('has-content'); // Make sure the height is correct setElementHeight(sections.feedback.$element); // Need to trigger resize when feedback has finished transitioning setTimeout(self.trigger.bind(self, 'resize'), 150); } return self; }; /** * Set the content of the explanation / feedback panel * * @param {Object} data * @param {string} data.correct * @param {string} data.wrong * @param {string} data.text * @param {string} title Title for explanation panel * * @return {H5P.Question} */ self.setExplanation = function (data, title) { if (data) { var explainer = new H5P.Question.Explainer(title, data); if (sections.explanation) { // Update section update('explanation', explainer.getElement()); } else { register('explanation', explainer.getElement()); if (initialized && $wrapper) { insert(self.order, 'explanation', sections, $wrapper); } } } else if (sections.explanation) { // Hide explanation section sections.explanation.$element.children().detach(); } return self; }; /** * Checks to see if button is registered. * * @param {string} id * @returns {boolean} */ self.hasButton = function (id) { return (buttons[id] !== undefined); }; /** * @typedef {Object} ConfirmationDialog * @property {boolean} [enable] Must be true to show confirmation dialog * @property {Object} [instance] Instance that uses confirmation dialog * @property {jQuery} [$parentElement] Append to this element. * @property {Object} [l10n] Translatable fields * @property {string} [l10n.header] Header text * @property {string} [l10n.body] Body text * @property {string} [l10n.cancelLabel] * @property {string} [l10n.confirmLabel] */ /** * Register buttons for the task. * * @param {string} id * @param {string} text label * @param {function} clicked * @param {boolean} [visible=true] * @param {Object} [options] Options for button * @param {Object} [extras] Extra options * @param {ConfirmationDialog} [extras.confirmationDialog] Confirmation dialog */ self.addButton = function (id, text, clicked, visible, options, extras) { if (buttons[id]) { return self; // Already registered } if (sections.buttons === undefined) { // We have buttons, register wrapper register('buttons'); if (initialized) { insert(self.order, 'buttons', sections, $wrapper); } } extras = extras || {}; extras.confirmationDialog = extras.confirmationDialog || {}; options = options || {}; var confirmationDialog = self.addConfirmationDialogToButton(extras.confirmationDialog, clicked); /** * Handle button clicks through both mouse and keyboard * @private */ var handleButtonClick = function () { if (extras.confirmationDialog.enable && confirmationDialog) { // Show popups section if used if (!extras.confirmationDialog.$parentElement) { sections.popups.$element.removeClass('hidden'); } confirmationDialog.show($e.position().top); } else { clicked(); } }; buttons[id] = { isTruncated: false, text: text, isVisible: false }; // The button might be <button> or <a> // (dependent on options.href set or not) var isAnchorTag = (options.href !== undefined); var $e = buttons[id].$element = JoubelUI.createButton($.extend({ 'class': 'h5p-question-' + id, html: text, title: text, on: { click: function (event) { handleButtonClick(); if (isAnchorTag) { event.preventDefault(); } } } }, options)); buttonOrder.push(id); // The button might be <button> or <a>. If <a>, the space key is not // triggering the click event, must therefore handle this here: if (isAnchorTag) { $e.on('keypress', function (event) { if (event.which === 32) { // Space handleButtonClick(); event.preventDefault(); } }); } if (visible === undefined || visible) { // Button should be visible $e.appendTo(sections.buttons.$element); buttons[id].isVisible = true; showSection(sections.buttons); } return self; }; var setButtonWidth = function (button) { var $button = button.$element; var $tmp = $button.clone() .css({ 'position': 'absolute', 'white-space': 'nowrap', 'max-width': 'none' }).removeClass('truncated') .html(button.text) .appendTo($button.parent()); // Calculate max width (button including text) button.width = { max: Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right'))) }; // Calculate min width (truncated, icon only) $tmp.html('').addClass('truncated'); button.width.min = Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right'))); $tmp.remove(); }; /** * Add confirmation dialog to button * @param {ConfirmationDialog} options * A confirmation dialog that will be shown before click handler of button * is triggered * @param {function} clicked * Click handler of button * @return {H5P.ConfirmationDialog|undefined} * Confirmation dialog if enabled */ self.addConfirmationDialogToButton = function (options, clicked) { options = options || {}; if (!options.enable) { return; } // Confirmation dialog var confirmationDialog = new H5P.ConfirmationDialog({ instance: options.instance, headerText: options.l10n.header, dialogText: options.l10n.body, cancelText: options.l10n.cancelLabel, confirmText: options.l10n.confirmLabel }); // Determine parent element if (options.$parentElement) { confirmationDialog.appendTo(options.$parentElement.get(0)); } else { // Create popup section and append to that if (sections.popups === undefined) { register('popups'); if (initialized) { insert(self.order, 'popups', sections, $wrapper); } sections.popups.$element.addClass('hidden'); self.order.push('popups'); } confirmationDialog.appendTo(sections.popups.$element.get(0)); } // Add event listeners confirmationDialog.on('confirmed', function () { if (!options.$parentElement) { sections.popups.$element.addClass('hidden'); } clicked(); // Trigger to content type self.trigger('confirmed'); }); confirmationDialog.on('canceled', function () { if (!options.$parentElement) { sections.popups.$element.addClass('hidden'); } // Trigger to content type self.trigger('canceled'); }); return confirmationDialog; }; /** * Show registered button with given identifier. * * @param {string} id * @param {Number} [priority] */ self.showButton = function (id, priority) { var aboutToBeHidden = existsInArray(id, 'id', buttonsToHide) !== -1; if (buttons[id] === undefined || (buttons[id].isVisible === true && !aboutToBeHidden)) { return self; } priority = priority || 0; // Skip if already being shown var indexToShow = existsInArray(id, 'id', buttonsToShow); if (indexToShow !== -1) { // Update priority if (buttonsToShow[indexToShow].priority < priority) { buttonsToShow[indexToShow].priority = priority; } return self; } // Check if button is going to be hidden on next tick var exists = existsInArray(id, 'id', buttonsToHide); if (exists !== -1) { // Skip hiding if higher priority if (buttonsToHide[exists].priority <= priority) { buttonsToHide.splice(exists, 1); buttonsToShow.push({id: id, priority: priority}); } } // If button is not shown else if (!buttons[id].$element.is(':visible')) { // Show button on next tick buttonsToShow.push({id: id, priority: priority}); } if (!toggleButtonsTimer) { toggleButtonsTimer = setTimeout(toggleButtons, 0); } return self; }; /** * Hide registered button with given identifier. * * @param {string} id * @param {number} [priority] */ self.hideButton = function (id, priority) { var aboutToBeShown = existsInArray(id, 'id', buttonsToShow) !== -1; if (buttons[id] === undefined || (buttons[id].isVisible === false && !aboutToBeShown)) { return self; } priority = priority || 0; // Skip if already being hidden var indexToHide = existsInArray(id, 'id', buttonsToHide); if (indexToHide !== -1) { // Update priority if (buttonsToHide[indexToHide].priority < priority) { buttonsToHide[indexToHide].priority = priority; } return self; } // Check if buttons is going to be shown on next tick var exists = existsInArray(id, 'id', buttonsToShow); if (exists !== -1) { // Skip showing if higher priority if (buttonsToShow[exists].priority <= priority) { buttonsToShow.splice(exists, 1); buttonsToHide.push({id: id, priority: priority}); } } else if (!buttons[id].$element.is(':visible')) { // Make sure it is detached in case the container is hidden. hideButton(id); } else { // Hide button on next tick. buttonsToHide.push({id: id, priority: priority}); } if (!toggleButtonsTimer) { toggleButtonsTimer = setTimeout(toggleButtons, 0); } return self; }; /** * Set focus to the given button. If no button is given the first visible * button gets focused. This is useful if you lose focus. * * @param {string} [id] */ self.focusButton = function (id) { if (id === undefined) { // Find first button that is visible. for (var i = 0; i < buttonOrder.length; i++) { var button = buttons[buttonOrder[i]]; if (button && button.isVisible) { // Give that button focus button.$element.focus(); break; } } } else if (buttons[id] && buttons[id].$element.is(':visible')) { // Set focus to requested button buttons[id].$element.focus(); } return self; }; /** * Toggle readspeaker functionality * @param {boolean} [disable] True to disable, false to enable. */ self.toggleReadSpeaker = function (disable) { behaviour.disableReadSpeaker = disable || !behaviour.disableReadSpeaker; }; /** * Set new element for section. * * @param {String} id * @param {H5P.jQuery} $element */ self.insertSectionAtElement = function (id, $element) { if (sections[id] === undefined) { register(id); } sections[id].parent = $element; // Insert section if question is not initialized if (!initialized) { insert([id], id, sections, $element); } return self; }; /** * Attach content to given container. * * @param {H5P.jQuery} $container */ self.attach = function ($container) { if (self.isRoot()) { self.setActivityStarted(); } // The first time we attach we also create our DOM elements. if ($wrapper === undefined) { if (self.registerDomElements !== undefined && (self.registerDomElements instanceof Function || typeof self.registerDomElements === 'function')) { // Give the question type a chance to register before attaching self.registerDomElements(); } // Create section for reading messages $read = $('<div/>', { 'aria-live': 'polite', 'class': 'h5p-hidden-read' }); register('read', $read); self.trigger('registerDomElements'); } // Prepare container $wrapper = $container; $container.html('') .addClass('h5p-question h5p-' + type); // Add sections in given order var $sections = []; for (var i = 0; i < self.order.length; i++) { var section = self.order[i]; if (sections[section]) { if (sections[section].parent) { // Section has a different parent sections[section].$element.appendTo(sections[section].parent); } else { $sections.push(sections[section].$element); } sections[section].isVisible = true; } } // Only append once to DOM for optimal performance $container.append($sections); // Let others react to dom changes self.trigger('domChanged', { '$target': $container, 'library': self.libraryInfo.machineName, 'contentId': self.contentId, 'key': 'newLibrary' }, {'bubbles': true, 'external': true}); // ?? initialized = true; return self; }; /** * Detach all sections from their parents */ self.detachSections = function () { // Deinit Question initialized = false; // Detach sections for (var section in sections) { sections[section].$element.detach(); } return self; }; // Listen for resize self.on('resize', function () { // Allow elements to attach and set their height before resizing if (!sectionsIsTransitioning && sections.feedback && showFeedback) { // Resize feedback to fit setElementHeight(sections.feedback.$element); } // Re-position feedback popup if in use var $element = sections.feedback; var $click = clickElement; if ($element != null && $element.$element != null && $click != null && $click.$element != null) { setTimeout(function () { positionFeedbackPopup($element.$element, $click.$element); }, 10); } resizeButtons(); }); } // Inheritance Question.prototype = Object.create(EventDispatcher.prototype); Question.prototype.constructor = Question; /** * Determine the overall feedback to display for the question. * Returns empty string if no matching range is found. * * @param {Object[]} feedbacks * @param {number} scoreRatio * @return {string} */ Question.determineOverallFeedback = function (feedbacks, scoreRatio) { scoreRatio = Math.floor(scoreRatio * 100); for (var i = 0; i < feedbacks.length; i++) { var feedback = feedbacks[i]; var hasFeedback = (feedback.feedback !== undefined && feedback.feedback.trim().length !== 0); if (feedback.from <= scoreRatio && feedback.to >= scoreRatio && hasFeedback) { return feedback.feedback; } } return ''; }; return Question; })(H5P.jQuery, H5P.EventDispatcher, H5P.JoubelUI); ; H5P.Question.Explainer = (function ($) { /** * Constructor * * @class * @param {string} title * @param {array} explanations */ function Explainer(title, explanations) { var self = this; /** * Create the DOM structure */ var createHTML = function () { self.$explanation = $('<div>', { 'class': 'h5p-question-explanation-container' }); // Add title: $('<div>', { 'class': 'h5p-question-explanation-title', role: 'heading', html: title, appendTo: self.$explanation }); var $explanationList = $('<ul>', { 'class': 'h5p-question-explanation-list', appendTo: self.$explanation }); for (var i = 0; i < explanations.length; i++) { var feedback = explanations[i]; var $explanationItem = $('<li>', { 'class': 'h5p-question-explanation-item', appendTo: $explanationList }); var $content = $('<div>', { 'class': 'h5p-question-explanation-status' }); if (feedback.correct) { $('<span>', { 'class': 'h5p-question-explanation-correct', html: feedback.correct, appendTo: $content }); } if (feedback.wrong) { $('<span>', { 'class': 'h5p-question-explanation-wrong', html: feedback.wrong, appendTo: $content }); } $content.appendTo($explanationItem); if (feedback.text) { $('<div>', { 'class': 'h5p-question-explanation-text', html: feedback.text, appendTo: $explanationItem }); } } }; createHTML(); /** * Return the container HTMLElement * * @return {HTMLElement} */ self.getElement = function () { return self.$explanation; }; } return Explainer; })(H5P.jQuery); ; (function (Question) { /** * Makes it easy to add animated score points for your question type. * * @class H5P.Question.ScorePoints */ Question.ScorePoints = function () { var self = this; var elements = []; var showElementsTimer; /** * Create the element that displays the score point element for questions. * * @param {boolean} isCorrect * @return {HTMLElement} */ self.getElement = function (isCorrect) { var element = document.createElement('div'); element.classList.add(isCorrect ? 'h5p-question-plus-one' : 'h5p-question-minus-one'); element.classList.add('h5p-question-hidden-one'); elements.push(element); // Schedule display animation of all added elements if (showElementsTimer) { clearTimeout(showElementsTimer); } showElementsTimer = setTimeout(showElements, 0); return element; }; /** * @private */ var showElements = function () { // Determine delay between triggering animations var delay = 0; var increment = 150; var maxTime = 1000; if (elements.length && elements.length > Math.ceil(maxTime / increment)) { // Animations will run for more than ~1 second, reduce it. increment = maxTime / elements.length; } for (var i = 0; i < elements.length; i++) { // Use timer to trigger show setTimeout(showElement(elements[i]), delay); // Increse delay for next element delay += increment; } }; /** * Trigger transition animation for the given element * * @private * @param {HTMLElement} element * @return {function} */ var showElement = function (element) { return function () { element.classList.remove('h5p-question-hidden-one'); }; }; }; })(H5P.Question); ; /*! For license information please see h5p-multi-media-choice.js.LICENSE.txt */ (()=>{var t={540:(t,e,i)=>{var n,o;!function(s,r){"use strict";void 0===(o="function"==typeof(n=r)?n.call(e,i,e,t):n)||(t.exports=o)}(window,(function(){"use strict";var t=function(){var t=window.Element.prototype;if(t.matches)return"matches";if(t.matchesSelector)return"matchesSelector";for(var e=["webkit","moz","ms","o"],i=0;i<e.length;i++){var n=e[i]+"MatchesSelector";if(t[n])return n}}();return function(e,i){return e[t](i)}}))},138:function(t,e,i){var n,o;"undefined"!=typeof window&&window,void 0===(o="function"==typeof(n=function(){"use strict";function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{};return(i[t]=i[t]||{})[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;o<i.length;o++){var s=i[o];n&&n[s]&&(this.off(t,s),delete n[s]),s.apply(this,e)}return this}},e.allOff=function(){delete this._events,delete this._onceEvents},t})?n.call(e,i,e,t):n)||(t.exports=o)},709:(t,e,i)=>{var n,o;!function(s,r){n=[i(540)],void 0===(o=function(t){return function(t,e){"use strict";var i={extend:function(t,e){for(var i in e)t[i]=e[i];return t},modulo:function(t,e){return(t%e+e)%e}},n=Array.prototype.slice;i.makeArray=function(t){return Array.isArray(t)?t:null==t?[]:"object"==typeof t&&"number"==typeof t.length?n.call(t):[t]},i.removeFrom=function(t,e){var i=t.indexOf(e);-1!=i&&t.splice(i,1)},i.getParent=function(t,i){for(;t.parentNode&&t!=document.body;)if(t=t.parentNode,e(t,i))return t},i.getQueryElement=function(t){return"string"==typeof t?document.querySelector(t):t},i.handleEvent=function(t){var e="on"+t.type;this[e]&&this[e](t)},i.filterFindElements=function(t,n){t=i.makeArray(t);var o=[];return t.forEach((function(t){if(t instanceof HTMLElement)if(n){e(t,n)&&o.push(t);for(var i=t.querySelectorAll(n),s=0;s<i.length;s++)o.push(i[s])}else o.push(t)})),o},i.debounceMethod=function(t,e,i){i=i||100;var n=t.prototype[e],o=e+"Timeout";t.prototype[e]=function(){var t=this[o];clearTimeout(t);var e=arguments,s=this;this[o]=setTimeout((function(){n.apply(s,e),delete s[o]}),i)}},i.docReady=function(t){var e=document.readyState;"complete"==e||"interactive"==e?setTimeout(t):document.addEventListener("DOMContentLoaded",t)},i.toDashed=function(t){return t.replace(/(.)([A-Z])/g,(function(t,e,i){return e+"-"+i})).toLowerCase()};var o=t.console;return i.htmlInit=function(e,n){i.docReady((function(){var s=i.toDashed(n),r="data-"+s,a=document.querySelectorAll("["+r+"]"),h=document.querySelectorAll(".js-"+s),c=i.makeArray(a).concat(i.makeArray(h)),l=r+"-options",u=t.jQuery;c.forEach((function(t){var i,s=t.getAttribute(r)||t.getAttribute(l);try{i=s&&JSON.parse(s)}catch(e){return void(o&&o.error("Error parsing "+r+" on "+t.className+": "+e))}var a=new e(t,i);u&&u.data(t,n,a)}))}))},i}(s,t)}.apply(e,n))||(t.exports=o)}(window)},252:(t,e,i)=>{var n,o;window,void 0===(o="function"==typeof(n=function(){"use strict";function t(t){var e=parseFloat(t);return-1==t.indexOf("%")&&!isNaN(e)&&e}function e(){}var i="undefined"==typeof console?e:function(t){},n=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],o=n.length;function s(){for(var t={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},e=0;e<o;e++)t[n[e]]=0;return t}function r(t){var e=getComputedStyle(t);return e||i("Style returned "+e+". Are you running this code in a hidden iframe on Firefox? See https://bit.ly/getsizebug1"),e}var a,h=!1;function c(){if(!h){h=!0;var e=document.createElement("div");e.style.width="200px",e.style.padding="1px 2px 3px 4px",e.style.borderStyle="solid",e.style.borderWidth="1px 2px 3px 4px",e.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(e);var n=r(e);a=200==Math.round(t(n.width)),l.isBoxSizeOuter=a,i.removeChild(e)}}function l(e){if(c(),"string"==typeof e&&(e=document.querySelector(e)),e&&"object"==typeof e&&e.nodeType){var i=r(e);if("none"==i.display)return s();var h={};h.width=e.offsetWidth,h.height=e.offsetHeight;for(var l=h.isBorderBox="border-box"==i.boxSizing,u=0;u<o;u++){var d=n[u],p=i[d],m=parseFloat(p);h[d]=isNaN(m)?0:m}var f=h.paddingLeft+h.paddingRight,g=h.paddingTop+h.paddingBottom,y=h.marginLeft+h.marginRight,v=h.marginTop+h.marginBottom,b=h.borderLeftWidth+h.borderRightWidth,w=h.borderTopWidth+h.borderBottomWidth,S=l&&a,x=t(i.width);!1!==x&&(h.width=x+(S?0:f+b));var T=t(i.height);return!1!==T&&(h.height=T+(S?0:g+w)),h.innerWidth=h.width-(f+b),h.innerHeight=h.height-(g+w),h.outerWidth=h.width+y,h.outerHeight=h.height+v,h}}return l})?n.call(e,i,e,t):n)||(t.exports=o)},630:(t,e,i)=>{var n,o,s;window,o=[i(871),i(252)],void 0===(s="function"==typeof(n=function(t,e){"use strict";var i=t.create("masonry");i.compatOptions.fitWidth="isFitWidth";var n=i.prototype;return n._resetLayout=function(){this.getSize(),this._getMeasurement("columnWidth","outerWidth"),this._getMeasurement("gutter","outerWidth"),this.measureColumns(),this.colYs=[];for(var t=0;t<this.cols;t++)this.colYs.push(0);this.maxY=0,this.horizontalColIndex=0},n.measureColumns=function(){if(this.getContainerWidth(),!this.columnWidth){var t=this.items[0],i=t&&t.element;this.columnWidth=i&&e(i).outerWidth||this.containerWidth}var n=this.columnWidth+=this.gutter,o=this.containerWidth+this.gutter,s=o/n,r=n-o%n;s=Math[r&&r<1?"round":"floor"](s),this.cols=Math.max(s,1)},n.getContainerWidth=function(){var t=this._getOption("fitWidth")?this.element.parentNode:this.element,i=e(t);this.containerWidth=i&&i.innerWidth},n._getItemLayoutPosition=function(t){t.getSize();var e=t.size.outerWidth%this.columnWidth,i=Math[e&&e<1?"round":"ceil"](t.size.outerWidth/this.columnWidth);i=Math.min(i,this.cols);for(var n=this[this.options.horizontalOrder?"_getHorizontalColPosition":"_getTopColPosition"](i,t),o={x:this.columnWidth*n.col,y:n.y},s=n.y+t.size.outerHeight,r=i+n.col,a=n.col;a<r;a++)this.colYs[a]=s;return o},n._getTopColPosition=function(t){var e=this._getTopColGroup(t),i=Math.min.apply(Math,e);return{col:e.indexOf(i),y:i}},n._getTopColGroup=function(t){if(t<2)return this.colYs;for(var e=[],i=this.cols+1-t,n=0;n<i;n++)e[n]=this._getColGroupY(n,t);return e},n._getColGroupY=function(t,e){if(e<2)return this.colYs[t];var i=this.colYs.slice(t,t+e);return Math.max.apply(Math,i)},n._getHorizontalColPosition=function(t,e){var i=this.horizontalColIndex%this.cols;i=t>1&&i+t>this.cols?0:i;var n=e.size.outerWidth&&e.size.outerHeight;return this.horizontalColIndex=n?i+t:this.horizontalColIndex,{col:i,y:this._getColGroupY(i,t)}},n._manageStamp=function(t){var i=e(t),n=this._getElementOffset(t),o=this._getOption("originLeft")?n.left:n.right,s=o+i.outerWidth,r=Math.floor(o/this.columnWidth);r=Math.max(0,r);var a=Math.floor(s/this.columnWidth);a-=s%this.columnWidth?0:1,a=Math.min(this.cols-1,a);for(var h=(this._getOption("originTop")?n.top:n.bottom)+i.outerHeight,c=r;c<=a;c++)this.colYs[c]=Math.max(h,this.colYs[c])},n._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var t={height:this.maxY};return this._getOption("fitWidth")&&(t.width=this._getContainerFitWidth()),t},n._getContainerFitWidth=function(){for(var t=0,e=this.cols;--e&&0===this.colYs[e];)t++;return(this.cols-t)*this.columnWidth-this.gutter},n.needsResizeLayout=function(){var t=this.containerWidth;return this.getContainerWidth(),t!=this.containerWidth},i})?n.apply(e,o):n)||(t.exports=s)},260:(t,e,i)=>{var n,o,s;window,o=[i(138),i(252)],void 0===(s="function"==typeof(n=function(t,e){"use strict";function i(t){for(var e in t)return!1;return!0}var n=document.documentElement.style,o="string"==typeof n.transition?"transition":"WebkitTransition",s="string"==typeof n.transform?"transform":"WebkitTransform",r={WebkitTransition:"webkitTransitionEnd",transition:"transitionend"}[o],a={transform:s,transition:o,transitionDuration:o+"Duration",transitionProperty:o+"Property",transitionDelay:o+"Delay"};function h(t,e){t&&(this.element=t,this.layout=e,this.position={x:0,y:0},this._create())}var c=h.prototype=Object.create(t.prototype);function l(t){return t.replace(/([A-Z])/g,(function(t){return"-"+t.toLowerCase()}))}c.constructor=h,c._create=function(){this._transn={ingProperties:{},clean:{},onEnd:{}},this.css({position:"absolute"})},c.handleEvent=function(t){var e="on"+t.type;this[e]&&this[e](t)},c.getSize=function(){this.size=e(this.element)},c.css=function(t){var e=this.element.style;for(var i in t)e[a[i]||i]=t[i]},c.getPosition=function(){var t=getComputedStyle(this.element),e=this.layout._getOption("originLeft"),i=this.layout._getOption("originTop"),n=t[e?"left":"right"],o=t[i?"top":"bottom"],s=parseFloat(n),r=parseFloat(o),a=this.layout.size;-1!=n.indexOf("%")&&(s=s/100*a.width),-1!=o.indexOf("%")&&(r=r/100*a.height),s=isNaN(s)?0:s,r=isNaN(r)?0:r,s-=e?a.paddingLeft:a.paddingRight,r-=i?a.paddingTop:a.paddingBottom,this.position.x=s,this.position.y=r},c.layoutPosition=function(){var t=this.layout.size,e={},i=this.layout._getOption("originLeft"),n=this.layout._getOption("originTop"),o=i?"paddingLeft":"paddingRight",s=i?"left":"right",r=i?"right":"left",a=this.position.x+t[o];e[s]=this.getXValue(a),e[r]="";var h=n?"paddingTop":"paddingBottom",c=n?"top":"bottom",l=n?"bottom":"top",u=this.position.y+t[h];e[c]=this.getYValue(u),e[l]="",this.css(e),this.emitEvent("layout",[this])},c.getXValue=function(t){var e=this.layout._getOption("horizontal");return this.layout.options.percentPosition&&!e?t/this.layout.size.width*100+"%":t+"px"},c.getYValue=function(t){var e=this.layout._getOption("horizontal");return this.layout.options.percentPosition&&e?t/this.layout.size.height*100+"%":t+"px"},c._transitionTo=function(t,e){this.getPosition();var i=this.position.x,n=this.position.y,o=t==this.position.x&&e==this.position.y;if(this.setPosition(t,e),!o||this.isTransitioning){var s=t-i,r=e-n,a={};a.transform=this.getTranslate(s,r),this.transition({to:a,onTransitionEnd:{transform:this.layoutPosition},isCleaning:!0})}else this.layoutPosition()},c.getTranslate=function(t,e){return"translate3d("+(t=this.layout._getOption("originLeft")?t:-t)+"px, "+(e=this.layout._getOption("originTop")?e:-e)+"px, 0)"},c.goTo=function(t,e){this.setPosition(t,e),this.layoutPosition()},c.moveTo=c._transitionTo,c.setPosition=function(t,e){this.position.x=parseFloat(t),this.position.y=parseFloat(e)},c._nonTransition=function(t){for(var e in this.css(t.to),t.isCleaning&&this._removeStyles(t.to),t.onTransitionEnd)t.onTransitionEnd[e].call(this)},c.transition=function(t){if(parseFloat(this.layout.options.transitionDuration)){var e=this._transn;for(var i in t.onTransitionEnd)e.onEnd[i]=t.onTransitionEnd[i];for(i in t.to)e.ingProperties[i]=!0,t.isCleaning&&(e.clean[i]=!0);t.from&&(this.css(t.from),this.element.offsetHeight),this.enableTransition(t.to),this.css(t.to),this.isTransitioning=!0}else this._nonTransition(t)};var u="opacity,"+l(s);c.enableTransition=function(){if(!this.isTransitioning){var t=this.layout.options.transitionDuration;t="number"==typeof t?t+"ms":t,this.css({transitionProperty:u,transitionDuration:t,transitionDelay:this.staggerDelay||0}),this.element.addEventListener(r,this,!1)}},c.onwebkitTransitionEnd=function(t){this.ontransitionend(t)},c.onotransitionend=function(t){this.ontransitionend(t)};var d={"-webkit-transform":"transform"};c.ontransitionend=function(t){if(t.target===this.element){var e=this._transn,n=d[t.propertyName]||t.propertyName;delete e.ingProperties[n],i(e.ingProperties)&&this.disableTransition(),n in e.clean&&(this.element.style[t.propertyName]="",delete e.clean[n]),n in e.onEnd&&(e.onEnd[n].call(this),delete e.onEnd[n]),this.emitEvent("transitionEnd",[this])}},c.disableTransition=function(){this.removeTransitionStyles(),this.element.removeEventListener(r,this,!1),this.isTransitioning=!1},c._removeStyles=function(t){var e={};for(var i in t)e[i]="";this.css(e)};var p={transitionProperty:"",transitionDuration:"",transitionDelay:""};return c.removeTransitionStyles=function(){this.css(p)},c.stagger=function(t){t=isNaN(t)?0:t,this.staggerDelay=t+"ms"},c.removeElem=function(){this.element.parentNode.removeChild(this.element),this.css({display:""}),this.emitEvent("remove",[this])},c.remove=function(){o&&parseFloat(this.layout.options.transitionDuration)?(this.once("transitionEnd",(function(){this.removeElem()})),this.hide()):this.removeElem()},c.reveal=function(){delete this.isHidden,this.css({display:""});var t=this.layout.options,e={};e[this.getHideRevealTransitionEndProperty("visibleStyle")]=this.onRevealTransitionEnd,this.transition({from:t.hiddenStyle,to:t.visibleStyle,isCleaning:!0,onTransitionEnd:e})},c.onRevealTransitionEnd=function(){this.isHidden||this.emitEvent("reveal")},c.getHideRevealTransitionEndProperty=function(t){var e=this.layout.options[t];if(e.opacity)return"opacity";for(var i in e)return i},c.hide=function(){this.isHidden=!0,this.css({display:""});var t=this.layout.options,e={};e[this.getHideRevealTransitionEndProperty("hiddenStyle")]=this.onHideTransitionEnd,this.transition({from:t.visibleStyle,to:t.hiddenStyle,isCleaning:!0,onTransitionEnd:e})},c.onHideTransitionEnd=function(){this.isHidden&&(this.css({display:"none"}),this.emitEvent("hide"))},c.destroy=function(){this.css({position:"",left:"",right:"",top:"",bottom:"",transition:"",transform:""})},h})?n.apply(e,o):n)||(t.exports=s)},871:(t,e,i)=>{var n,o;!function(s,r){"use strict";n=[i(138),i(252),i(709),i(260)],void 0===(o=function(t,e,i,n){return function(t,e,i,n,o){var s=t.console,r=t.jQuery,a=function(){},h=0,c={};function l(t,e){var i=n.getQueryElement(t);if(i){this.element=i,r&&(this.$element=r(this.element)),this.options=n.extend({},this.constructor.defaults),this.option(e);var o=++h;this.element.outlayerGUID=o,c[o]=this,this._create(),this._getOption("initLayout")&&this.layout()}else s&&s.error("Bad element for "+this.constructor.namespace+": "+(i||t))}l.namespace="outlayer",l.Item=o,l.defaults={containerStyle:{position:"relative"},initLayout:!0,originLeft:!0,originTop:!0,resize:!0,resizeContainer:!0,transitionDuration:"0.4s",hiddenStyle:{opacity:0,transform:"scale(0.001)"},visibleStyle:{opacity:1,transform:"scale(1)"}};var u=l.prototype;function d(t){function e(){t.apply(this,arguments)}return e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e}n.extend(u,e.prototype),u.option=function(t){n.extend(this.options,t)},u._getOption=function(t){var e=this.constructor.compatOptions[t];return e&&void 0!==this.options[e]?this.options[e]:this.options[t]},l.compatOptions={initLayout:"isInitLayout",horizontal:"isHorizontal",layoutInstant:"isLayoutInstant",originLeft:"isOriginLeft",originTop:"isOriginTop",resize:"isResizeBound",resizeContainer:"isResizingContainer"},u._create=function(){this.reloadItems(),this.stamps=[],this.stamp(this.options.stamp),n.extend(this.element.style,this.options.containerStyle),this._getOption("resize")&&this.bindResize()},u.reloadItems=function(){this.items=this._itemize(this.element.children)},u._itemize=function(t){for(var e=this._filterFindItemElements(t),i=this.constructor.Item,n=[],o=0;o<e.length;o++){var s=new i(e[o],this);n.push(s)}return n},u._filterFindItemElements=function(t){return n.filterFindElements(t,this.options.itemSelector)},u.getItemElements=function(){return this.items.map((function(t){return t.element}))},u.layout=function(){this._resetLayout(),this._manageStamps();var t=this._getOption("layoutInstant"),e=void 0!==t?t:!this._isLayoutInited;this.layoutItems(this.items,e),this._isLayoutInited=!0},u._init=u.layout,u._resetLayout=function(){this.getSize()},u.getSize=function(){this.size=i(this.element)},u._getMeasurement=function(t,e){var n,o=this.options[t];o?("string"==typeof o?n=this.element.querySelector(o):o instanceof HTMLElement&&(n=o),this[t]=n?i(n)[e]:o):this[t]=0},u.layoutItems=function(t,e){t=this._getItemsForLayout(t),this._layoutItems(t,e),this._postLayout()},u._getItemsForLayout=function(t){return t.filter((function(t){return!t.isIgnored}))},u._layoutItems=function(t,e){if(this._emitCompleteOnItems("layout",t),t&&t.length){var i=[];t.forEach((function(t){var n=this._getItemLayoutPosition(t);n.item=t,n.isInstant=e||t.isLayoutInstant,i.push(n)}),this),this._processLayoutQueue(i)}},u._getItemLayoutPosition=function(){return{x:0,y:0}},u._processLayoutQueue=function(t){this.updateStagger(),t.forEach((function(t,e){this._positionItem(t.item,t.x,t.y,t.isInstant,e)}),this)},u.updateStagger=function(){var t=this.options.stagger;if(null!=t)return this.stagger=m(t),this.stagger;this.stagger=0},u._positionItem=function(t,e,i,n,o){n?t.goTo(e,i):(t.stagger(o*this.stagger),t.moveTo(e,i))},u._postLayout=function(){this.resizeContainer()},u.resizeContainer=function(){if(this._getOption("resizeContainer")){var t=this._getContainerSize();t&&(this._setContainerMeasure(t.width,!0),this._setContainerMeasure(t.height,!1))}},u._getContainerSize=a,u._setContainerMeasure=function(t,e){if(void 0!==t){var i=this.size;i.isBorderBox&&(t+=e?i.paddingLeft+i.paddingRight+i.borderLeftWidth+i.borderRightWidth:i.paddingBottom+i.paddingTop+i.borderTopWidth+i.borderBottomWidth),t=Math.max(t,0),this.element.style[e?"width":"height"]=t+"px"}},u._emitCompleteOnItems=function(t,e){var i=this;function n(){i.dispatchEvent(t+"Complete",null,[e])}var o=e.length;if(e&&o){var s=0;e.forEach((function(e){e.once(t,r)}))}else n();function r(){++s==o&&n()}},u.dispatchEvent=function(t,e,i){var n=e?[e].concat(i):i;if(this.emitEvent(t,n),r)if(this.$element=this.$element||r(this.element),e){var o=r.Event(e);o.type=t,this.$element.trigger(o,i)}else this.$element.trigger(t,i)},u.ignore=function(t){var e=this.getItem(t);e&&(e.isIgnored=!0)},u.unignore=function(t){var e=this.getItem(t);e&&delete e.isIgnored},u.stamp=function(t){(t=this._find(t))&&(this.stamps=this.stamps.concat(t),t.forEach(this.ignore,this))},u.unstamp=function(t){(t=this._find(t))&&t.forEach((function(t){n.removeFrom(this.stamps,t),this.unignore(t)}),this)},u._find=function(t){if(t)return"string"==typeof t&&(t=this.element.querySelectorAll(t)),t=n.makeArray(t)},u._manageStamps=function(){this.stamps&&this.stamps.length&&(this._getBoundingRect(),this.stamps.forEach(this._manageStamp,this))},u._getBoundingRect=function(){var t=this.element.getBoundingClientRect(),e=this.size;this._boundingRect={left:t.left+e.paddingLeft+e.borderLeftWidth,top:t.top+e.paddingTop+e.borderTopWidth,right:t.right-(e.paddingRight+e.borderRightWidth),bottom:t.bottom-(e.paddingBottom+e.borderBottomWidth)}},u._manageStamp=a,u._getElementOffset=function(t){var e=t.getBoundingClientRect(),n=this._boundingRect,o=i(t);return{left:e.left-n.left-o.marginLeft,top:e.top-n.top-o.marginTop,right:n.right-e.right-o.marginRight,bottom:n.bottom-e.bottom-o.marginBottom}},u.handleEvent=n.handleEvent,u.bindResize=function(){t.addEventListener("resize",this),this.isResizeBound=!0},u.unbindResize=function(){t.removeEventListener("resize",this),this.isResizeBound=!1},u.onresize=function(){this.resize()},n.debounceMethod(l,"onresize",100),u.resize=function(){this.isResizeBound&&this.needsResizeLayout()&&this.layout()},u.needsResizeLayout=function(){var t=i(this.element);return this.size&&t&&t.innerWidth!==this.size.innerWidth},u.addItems=function(t){var e=this._itemize(t);return e.length&&(this.items=this.items.concat(e)),e},u.appended=function(t){var e=this.addItems(t);e.length&&(this.layoutItems(e,!0),this.reveal(e))},u.prepended=function(t){var e=this._itemize(t);if(e.length){var i=this.items.slice(0);this.items=e.concat(i),this._resetLayout(),this._manageStamps(),this.layoutItems(e,!0),this.reveal(e),this.layoutItems(i)}},u.reveal=function(t){if(this._emitCompleteOnItems("reveal",t),t&&t.length){var e=this.updateStagger();t.forEach((function(t,i){t.stagger(i*e),t.reveal()}))}},u.hide=function(t){if(this._emitCompleteOnItems("hide",t),t&&t.length){var e=this.updateStagger();t.forEach((function(t,i){t.stagger(i*e),t.hide()}))}},u.revealItemElements=function(t){var e=this.getItems(t);this.reveal(e)},u.hideItemElements=function(t){var e=this.getItems(t);this.hide(e)},u.getItem=function(t){for(var e=0;e<this.items.length;e++){var i=this.items[e];if(i.element==t)return i}},u.getItems=function(t){t=n.makeArray(t);var e=[];return t.forEach((function(t){var i=this.getItem(t);i&&e.push(i)}),this),e},u.remove=function(t){var e=this.getItems(t);this._emitCompleteOnItems("remove",e),e&&e.length&&e.forEach((function(t){t.remove(),n.removeFrom(this.items,t)}),this)},u.destroy=function(){var t=this.element.style;t.height="",t.position="",t.width="",this.items.forEach((function(t){t.destroy()})),this.unbindResize();var e=this.element.outlayerGUID;delete c[e],delete this.element.outlayerGUID,r&&r.removeData(this.element,this.constructor.namespace)},l.data=function(t){var e=(t=n.getQueryElement(t))&&t.outlayerGUID;return e&&c[e]},l.create=function(t,e){var i=d(l);return i.defaults=n.extend({},l.defaults),n.extend(i.defaults,e),i.compatOptions=n.extend({},l.compatOptions),i.namespace=t,i.data=l.data,i.Item=d(o),n.htmlInit(i,t),r&&r.bridget&&r.bridget(t,i),i};var p={ms:1,s:1e3};function m(t){if("number"==typeof t)return t;var e=t.match(/(^\d*\.?\d*)(\w*)/),i=e&&e[1],n=e&&e[2];return i.length?(i=parseFloat(i))*(p[n]||1):0}return l.Item=o,l}(s,t,e,i,n)}.apply(e,n))||(t.exports=o)}(window)}},e={};function i(n){var o=e[n];if(void 0!==o)return o.exports;var s=e[n]={exports:{}};return t[n].call(s.exports,s,s.exports,i),s.exports}(()=>{"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=function(){function e(){}return e.extendParams=function(t){return e.deepExtend({question:"No question text provided",behaviour:{enableSolutionsButton:!0,enableRetry:!0,questionType:"auto",confirmCheckDialog:!1,confirmRetryDialog:!1,aspectRatio:"auto",maxAlternativesPerRow:4},l10n:{checkAnswerButtonText:"Check",checkAnswer:"Check the answers. The responses will be marked as correct, incorrect, or unanswered.",showSolutionButtonText:"Show solution",showSolution:"Show the solution. The task will be marked with its correct solution.",correctAnswer:"Correct answer",wrongAnswer:"Wrong answer",shouldCheck:"Should have been checked",shouldNotCheck:"Should not have been checked",noAnswer:"Please answer before viewing the solution",retryText:"Retry",retry:"Retry the task. Reset all responses and start the task over again.",result:"You got :num out of :total points",confirmCheck:{header:"Finish?",body:"Are you sure you want to finish?",cancelLabel:"Cancel",confirmLabel:"Finish"},confirmRetry:{header:"Retry?",body:"Are you sure you wish to retry?",cancelLabel:"Cancel",confirmLabel:"Retry"},missingAltText:"Alt text missing"}},t)},e.deepExtend=function(i){i=i||{};for(var n=1;n<arguments.length;n++){var o=arguments[n];if(o)for(var s in Array.isArray(o)&&(i=o),o)Object.prototype.hasOwnProperty.call(o,s)&&("object"===t(o[s])?i[s]=e.deepExtend(i[s],o[s]):i[s]=o[s])}return i},e}(),n=function(t){var e=document.createElement("div");return e.innerHTML=t,e.textContent},o=function(){function t(t,e,i,n,o,s,r){this.contentId=e,this.aspectRatio=i,this.singleAnswer=n,this.assetsFilePath=o,this.missingAltText=s,this.media=t.media,this.correct=t.correct,this.callbacks=r||{},this.callbacks.onClick=this.callbacks.onClick||function(){},this.callbacks.onKeyboardSelect=this.callbacks.onKeyboardSelect||function(){},this.callbacks.onKeyboardArrowKey=this.callbacks.onKeyboardArrowKey||function(){},this.callbacks.triggerResize=this.callbacks.triggerResize||function(){},this.content=document.createElement("li"),this.content.classList.add("h5p-multi-media-choice-list-item"),this.wrapper=document.createElement("div"),this.wrapper.classList.add("h5p-multi-media-choice-option"),this.content.appendChild(this.wrapper),n?this.content.setAttribute("role","radio"):this.content.setAttribute("role","checkbox"),this.content.setAttribute("aria-checked","false"),this.enable(),this.content.addEventListener("click",this.callbacks.onClick);var a=this.createMediaContent();this.wrapper.appendChild(a),this.addKeyboardHandlers()}var e=t.prototype;return e.createMediaContent=function(){var t=document.createElement("div");switch(t.classList.add("h5p-multi-media-choice-media-wrapper"),"auto"!==this.aspectRatio&&(t.classList.add("h5p-multi-media-choice-media-wrapper-specific-ratio"),t.classList.add("h5p-multi-media-choice-media-wrapper-".concat(this.aspectRatio))),this.media.metadata.contentType){case"Image":return t.appendChild(this.buildImage(this.option)),t}},e.getDescription=function(){switch(this.media.metadata.contentType){case"Image":return this.media.params.alt||this.missingAltText;default:return""}},e.buildImage=function(){var t=this.media.params.alt?this.media.params.alt:"",e=this.media.params.title?this.media.params.title:"",i="";if(this.media.params.file)i=H5P.getPath(this.media.params.file.path,this.contentId);else{var o="auto"===this.aspectRatio?"1to1":this.aspectRatio;i="".concat(this.assetsFilePath,"/placeholder").concat(o,".svg")}var s=document.createElement("img");return s.setAttribute("src",i),this.content.setAttribute("aria-label",n(t)),s.addEventListener("load",this.callbacks.triggerResize),this.content.setAttribute("title",n(e)),s.classList.add("h5p-multi-media-choice-media"),"auto"!==this.aspectRatio&&s.classList.add("h5p-multi-media-choice-media-specific-ratio"),s},e.isSelected=function(){return"true"===this.content.getAttribute("aria-checked")},e.isCorrect=function(){return this.correct},e.isDisabled=function(){return"true"===this.content.getAttribute("aria-disabled")},e.getDOM=function(){return this.content},e.setTabIndex=function(t){switch(t){case-1:this.content.setAttribute("tabindex","-1");break;case 0:this.content.setAttribute("tabindex","0");break;default:this.content.setAttribute("tabindex","0")}},e.toggle=function(){this.isSelected()?(this.content.setAttribute("aria-checked","false"),this.wrapper.classList.remove("h5p-multi-media-choice-selected")):(this.content.setAttribute("aria-checked","true"),this.wrapper.classList.add("h5p-multi-media-choice-selected"))},e.uncheck=function(){this.content.setAttribute("aria-checked","false"),this.wrapper.classList.remove("h5p-multi-media-choice-selected")},e.focus=function(){this.content.focus()},e.enable=function(){this.content.setAttribute("aria-disabled","false"),this.wrapper.classList.add("h5p-multi-media-choice-enabled")},e.disable=function(){this.content.setAttribute("aria-disabled","true"),this.content.setAttribute("tabindex","-1"),this.wrapper.classList.remove("h5p-multi-media-choice-enabled")},e.showSelectedSolution=function(t){var e=t.correctAnswer,i=t.wrongAnswer;this.wrapper.classList.remove("h5p-multi-media-choice-selected"),this.isSelected()&&(this.correct?(this.wrapper.classList.add("h5p-multi-media-choice-correct"),this.addAccessibilitySolutionText(e)):(this.wrapper.classList.add("h5p-multi-media-choice-wrong"),this.addAccessibilitySolutionText(i)))},e.showUnselectedSolution=function(t){var e=t.shouldCheck,i=t.shouldNotCheck;this.isSelected()||(this.correct?(this.wrapper.classList.add("h5p-multi-media-choice-show-correct"),this.addAccessibilitySolutionText(e)):this.addAccessibilitySolutionText(i))},e.addAccessibilitySolutionText=function(t){this.accessibilitySolutionText=document.createElement("span"),this.accessibilitySolutionText.classList.add("hidden-accessibility-solution-text"),this.accessibilitySolutionText.innerText="".concat(t,"."),this.wrapper.appendChild(this.accessibilitySolutionText)},e.hideSolution=function(){this.wrapper.classList.remove("h5p-multi-media-choice-correct"),this.wrapper.classList.remove("h5p-multi-media-choice-show-correct"),this.wrapper.classList.remove("h5p-multi-media-choice-wrong"),this.accessibilitySolutionText&&this.accessibilitySolutionText.parentNode&&this.accessibilitySolutionText.parentNode.removeChild(this.accessibilitySolutionText)},e.addKeyboardHandlers=function(){var t=this;this.content.addEventListener("keydown",(function(e){switch(e.key){case"Enter":case" ":if(t.isDisabled())return;e.preventDefault(),t.callbacks.onKeyboardSelect(t);break;case"ArrowLeft":case"ArrowUp":if(!t.singleAnswer)return;if(e.preventDefault(),t.getDOM()===t.getDOM().parentNode.firstChild)return;t.callbacks.onKeyboardArrowKey(e.code.replace("Arrow",""));break;case"ArrowRight":case"ArrowDown":if(!t.singleAnswer)return;if(e.preventDefault(),t.getDOM()===t.getDOM().parentNode.lastChild)return;t.callbacks.onKeyboardArrowKey(e.code.replace("Arrow",""))}}))},t}(),s=i(630),r=function(){function t(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1?arguments[1]:void 0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=arguments.length>3?arguments[3]:void 0;if(this.params=e,this.contentId=i,this.callbacks=n,this.callbacks.triggerResize=this.callbacks.triggerResize||function(){},this.callbacks.triggerInteracted=this.callbacks.triggerInteracted||function(){},this.assetsFilePath=r,this.maxAlternativesPerRow=this.params.behaviour.maxAlternativesPerRow,this.numberOfCorrectOptions=e.options?e.options.filter((function(t){return t.correct})).length:0,this.isSingleAnswer="auto"===this.params.behaviour.questionType?1===this.numberOfCorrectOptions:"single"===this.params.behaviour.questionType,this.aspectRatio=this.params.behaviour.aspectRatio,this.lastSelectedRadioButtonOption=null,this.content=document.createElement("div"),this.content.classList.add("h5p-multi-media-choice-content"),!this.params.options||this.params.options.length<2){var a={media:{params:{contentName:"Image"},library:"H5P.Image",subContentId:e.contentId,metadata:{contentType:"Image",license:"U",title:"Untitled Image"}},correct:!1};this.params.options&&1===this.params.options.length?this.params.options.push(a):this.params.options=[{},{}].fill(a)}this.options=this.params.options?this.params.options.map((function(e,n){return new o(e,i,t.aspectRatio,t.isSingleAnswer,r,t.params.l10n.missingAltText,{onClick:function(){return t.toggleSelected(n)},onKeyboardSelect:function(){return t.toggleSelected(n)},onKeyboardArrowKey:function(e){return t.handleOptionArrowKey(n,e)},triggerResize:t.callbacks.triggerResize})})):[],this.optionList=this.buildOptionList(this.options),this.content.appendChild(this.optionList),this.setTabIndexes(),this.masonry=new s(this.optionList,{gutter:20,itemSelector:".h5p-multi-media-choice-list-item"})}var e=t.prototype;return e.buildOptionList=function(){var t=document.createElement("ul");return t.setAttribute("role",this.isSingleAnswer?"radiogroup":"group"),t.setAttribute("aria-labelledby","h5p-media-choice".concat(this.contentId)),t.classList.add("h5p-multi-media-choice-option-list"),this.options.forEach((function(e){t.appendChild(e.getDOM())})),t},e.getDOM=function(){return this.content},e.singleAnswer=function(){return this.isSingleAnswer},e.getOptions=function(){return this.options},e.getMaxScore=function(){return this.params.behaviour.singlePoint||this.isSingleAnswer||this.isBlankCorrect()?1:this.numberOfCorrectOptions},e.getScore=function(){if(this.params.behaviour.singlePoint&&0===this.params.behaviour.passPercentage)return 1;if(!this.isAnyAnswerSelected())return this.isBlankCorrect()?1:0;if(this.isSingleAnswer)return this.lastSelectedRadioButtonOption.isCorrect()?1:0;var t=0;return this.options.forEach((function(e){e.isSelected()&&(e.isCorrect()?t++:t--)})),t=Math.max(0,t),this.params.behaviour.singlePoint?100*t/this.numberOfCorrectOptions>=this.params.behaviour.passPercentage?1:0:t},e.getSelectedOptions=function(){return this.options.filter((function(t){return t.isSelected()}))},e.isAnyAnswerSelected=function(){return this.getSelectedOptions().length>0},e.isBlankCorrect=function(){return 0===this.numberOfCorrectOptions},e.isPassed=function(){return 100*this.getScore()/this.getMaxScore()>=this.params.behaviour.passPercentage},e.showSelectedSolutions=function(){var t=this;this.options.forEach((function(e){return e.showSelectedSolution({correctAnswer:t.params.l10n.correctAnswer,wrongAnswer:t.params.l10n.wrongAnswer})}))},e.showUnselectedSolutions=function(){var t=this;this.options.forEach((function(e){return e.showUnselectedSolution({shouldCheck:t.params.l10n.shouldCheck,shouldNotCheck:t.params.l10n.shouldNotCheck})}))},e.focusUnselectedSolution=function(){var t=document.getElementsByClassName("h5p-multi-media-choice-show-correct")[0];t&&t.parentNode&&t.parentNode.focus()},e.hideSolutions=function(){this.options.forEach((function(t){return t.hideSolution()}))},e.toggleSelected=function(t){var e=this.options[t];if(!e.isDisabled()){if(this.isSingleAnswer){if(e.isSelected())return;this.lastSelectedRadioButtonOption&&(this.lastSelectedRadioButtonOption.uncheck(),this.lastSelectedRadioButtonOption.setTabIndex(-1)),this.lastSelectedRadioButtonOption=e,this.lastSelectedRadioButtonOption.setTabIndex(0)}e.toggle(),this.callbacks.triggerInteracted()}},e.resetSelections=function(){this.lastSelectedRadioButtonOption=null,this.setTabIndexes(),this.options.forEach((function(t){t.uncheck(),t.enable()}))},e.disableSelectables=function(){this.options.forEach((function(t){return t.disable()})),this.setTabIndexes(-1)},e.setTabIndexes=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;this.isSingleAnswer?(this.options.forEach((function(e){return e.setTabIndex(null!==t?t:-1)})),this.options[0].setTabIndex(null!==t?t:0)):this.options.forEach((function(e){return e.setTabIndex(null!==t?t:0)}))},e.handleOptionArrowKey=function(t,e){if(["Left","Right","Up","Down"].includes(e)){var i=t+{Right:1,Down:1,Left:-1,Up:-1}[e];this.toggleSelected(i),this.options[i].focus(),this.options[i].setTabIndex(0),this.options[t].setTabIndex(-1)}},e.resizeGridItem=function(t,e){t.style.width=e+"px"},e.setColumnProperties=function(){for(var t=this.optionList.getBoundingClientRect().width/230,e=Math.floor(Math.min(t,this.maxAlternativesPerRow,this.options.length)),i=this.optionList.getBoundingClientRect().width/e-20,n=0;n<this.options.length;n++)this.resizeGridItem(this.options[n].getDOM(),i);this.masonry.layout()},e.getSelectedIndexes=function(){for(var t=[],e=0;e<this.options.length;e++){this.options[e].isSelected()&&t.push(e)}return t},t}();function a(t,e,i,o,s,r){var a=t.createXAPIEventTemplate("answered");return function(t,e,i){var o=t.getVerifiedStatementValue(["object","definition"]);o.description={"en-US":n(i)},o.type="http://adlnet.gov/expapi/activities/cmi.interaction",o.interactionType="choice",o.choices=function(t){return t.map((function(t,e){return{id:e.toString(),description:{"en-US":n(t.getDescription())}}}))}(e),o.correctResponsesPattern=[h(e)]}(a,i,e),a.setScoredResult(o,s,t,!0,r),function(t,e){t.data.statement.result.response=e.flatMap((function(t,e){return t.isSelected()?e:[]})).toString().replaceAll(",","[,]")}(a,i),a}function h(t){return t.flatMap((function(t,e){return t.isCorrect()?e:[]})).toString().replaceAll(",","[,]")}function c(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function l(t,e){return(l=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}var u=function(t){var i,n;function o(i,n){var o,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return(o=t.call(this,"multi-media-choice")||this).contentId=n,o.extras=s,o.params=e.extendParams(i),o.registerDomElements=function(){if(o.params.media&&o.params.media.type&&o.params.media.type.library){var t=o.params.media.type;t.library.includes("H5P.Image")?t.params.file&&o.setImage(t.params.file.path,{disableImageZooming:i.media.disableImageZooming||!1,alt:t.params.alt,title:t.params.title}):t.library.includes("H5P.Video")&&t.params.sources&&o.setVideo(t)}o.params.question&&(o.introduction=document.createElement("div"),o.introduction.innerHTML=o.params.question,o.introduction.setAttribute("id","h5p-media-choice".concat(n)),document.createElement("img").src=o.getLibraryFilePath("assets/placeholder1to1.svg"),o.setIntroduction(o.introduction));o.content=new r(o.params,n,{triggerResize:function(){o.trigger("resize")},triggerInteracted:function(){o.triggerXAPI("interacted")}},o.getLibraryFilePath("assets")),o.setContent(o.content.getDOM()),o.addButtons(),o.on("resize",(function(){return o.content.setColumnProperties()}))},o.getAnswersGiven=function(){return o.content.isAnyAnswerSelected()||o.content.isBlankCorrect()},o.getScore=function(){return o.content.getScore()},o.getMaxScore=function(){return o.content.getMaxScore()},o.handleRead=function(t){o.read(t)},o.checkAnswer=function(){o.content.disableSelectables();var t=o.getScore(),e=o.getMaxScore(),i=H5P.Question.determineOverallFeedback(o.params.overallFeedback,t/e);o.setFeedback(i,t,e,o.params.l10n.result),o.params.behaviour.enableSolutionsButton&&t!==e&&o.showButton("show-solution"),o.params.behaviour.enableRetry&&t!==e&&o.showButton("try-again"),o.hideButton("check-answer"),o.content.showSelectedSolutions(),o.trigger(a(c(o),o.params.question,o.content.getOptions(),o.getScore(),o.getMaxScore(),o.content.isPassed()))},o.showSolutions=function(){o.hideButton("check-answer"),o.hideButton("show-solution"),o.params.behaviour.showSolutionsRequiresInput&&!o.content.isAnyAnswerSelected()?(o.updateFeedbackContent(o.params.l10n.noAnswer),o.handleRead(o.params.l10n.noAnswer)):(o.content.showUnselectedSolutions(),o.content.focusUnselectedSolution()),o.trigger("resize")},o.resetTask=function(){o.content.resetSelections(),o.showButton("check-answer"),o.hideButton("try-again"),o.hideButton("show-solution"),o.content.hideSolutions(),o.removeFeedback()},o}n=t,(i=o).prototype=Object.create(n.prototype),i.prototype.constructor=i,l(i,n);var s=o.prototype;return s.addButtons=function(){var t=this;this.addButton("check-answer",this.params.l10n.checkAnswerButtonText,(function(){t.checkAnswer()}),!0,{"aria-label":this.params.l10n.checkAnswer},{confirmationDialog:{enable:this.params.behaviour.confirmCheckDialog,l10n:this.params.l10n.confirmCheck,instance:this}}),this.addButton("show-solution",this.params.l10n.showSolutionButtonText,(function(){t.showSolutions()}),!1,{"aria-label":this.params.l10n.showSolution},{}),this.addButton("try-again",this.params.l10n.retryText,(function(){t.resetTask()}),!1,{"aria-label":this.params.l10n.retry},{confirmationDialog:{enable:this.params.behaviour.confirmRetryDialog,l10n:this.params.l10n.confirmRetry,instance:this}})},s.getCurrentState=function(){return{answers:this.content.getSelectedIndexes()}},s.getXAPIData=function(){return t=this,e=this.params.question,i=this.content.getOptions(),n=this.getScore(),o=this.getMaxScore(),s=this.content.isPassed(),{statement:a(t,e,i,n,o,s).data.statement};var t,e,i,n,o,s},o}(H5P.Question);H5P=H5P||{},H5P.MultiMediaChoice=u})()})();;