var oldTether = window.Tether; !function(t,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e(require,exports,module):t.Tether=e()}(this,function(t,e,o){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t){var e=getComputedStyle(t),o=e.position;if("fixed"===o)return t;for(var i=t;i=i.parentNode;){var n=void 0;try{n=getComputedStyle(i)}catch(r){}if("undefined"==typeof n||null===n)return i;var s=n.overflow,a=n.overflowX,f=n.overflowY;if(/(auto|scroll)/.test(s+f+a)&&("absolute"!==o||["relative","absolute","fixed"].indexOf(n.position)>=0))return i}return document.body}function r(t){var e=void 0;t===document?(e=document,t=document.documentElement):e=t.ownerDocument;var o=e.documentElement,i={},n=t.getBoundingClientRect();for(var r in n)i[r]=n[r];var s=x(e);return i.top-=s.top,i.left-=s.left,"undefined"==typeof i.width&&(i.width=document.body.scrollWidth-i.left-i.right),"undefined"==typeof i.height&&(i.height=document.body.scrollHeight-i.top-i.bottom),i.top=i.top-o.clientTop,i.left=i.left-o.clientLeft,i.right=e.body.clientWidth-i.width-i.left,i.bottom=e.body.clientHeight-i.height-i.top,i}function s(t){return t.offsetParent||document.documentElement}function a(){var t=document.createElement("div");t.style.width="100%",t.style.height="200px";var e=document.createElement("div");f(e.style,{position:"absolute",top:0,left:0,pointerEvents:"none",visibility:"hidden",width:"200px",height:"150px",overflow:"hidden"}),e.appendChild(t),document.body.appendChild(e);var o=t.offsetWidth;e.style.overflow="scroll";var i=t.offsetWidth;o===i&&(i=e.clientWidth),document.body.removeChild(e);var n=o-i;return{width:n,height:n}}function f(){var t=void 0===arguments[0]?{}:arguments[0],e=[];return Array.prototype.push.apply(e,arguments),e.slice(1).forEach(function(e){if(e)for(var o in e)({}).hasOwnProperty.call(e,o)&&(t[o]=e[o])}),t}function h(t,e){if("undefined"!=typeof t.classList)e.split(" ").forEach(function(e){e.trim()&&t.classList.remove(e)});else{var o=new RegExp("(^| )"+e.split(" ").join("|")+"( |$)","gi"),i=u(t).replace(o," ");p(t,i)}}function l(t,e){if("undefined"!=typeof t.classList)e.split(" ").forEach(function(e){e.trim()&&t.classList.add(e)});else{h(t,e);var o=u(t)+(" "+e);p(t,o)}}function d(t,e){if("undefined"!=typeof t.classList)return t.classList.contains(e);var o=u(t);return new RegExp("(^| )"+e+"( |$)","gi").test(o)}function u(t){return t.className instanceof SVGAnimatedString?t.className.baseVal:t.className}function p(t,e){t.setAttribute("class",e)}function c(t,e,o){o.forEach(function(o){-1===e.indexOf(o)&&d(t,o)&&h(t,o)}),e.forEach(function(e){d(t,e)||l(t,e)})}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function g(t,e){var o=void 0===arguments[2]?1:arguments[2];return t+o>=e&&e>=t-o}function m(){return"undefined"!=typeof performance&&"undefined"!=typeof performance.now?performance.now():+new Date}function v(){for(var t={top:0,left:0},e=arguments.length,o=Array(e),i=0;e>i;i++)o[i]=arguments[i];return o.forEach(function(e){var o=e.top,i=e.left;"string"==typeof o&&(o=parseFloat(o,10)),"string"==typeof i&&(i=parseFloat(i,10)),t.top+=o,t.left+=i}),t}function y(t,e){return"string"==typeof t.left&&-1!==t.left.indexOf("%")&&(t.left=parseFloat(t.left,10)/100*e.width),"string"==typeof t.top&&-1!==t.top.indexOf("%")&&(t.top=parseFloat(t.top,10)/100*e.height),t}function b(t,e){return"scrollParent"===e?e=t.scrollParent:"window"===e&&(e=[pageXOffset,pageYOffset,innerWidth+pageXOffset,innerHeight+pageYOffset]),e===document&&(e=e.documentElement),"undefined"!=typeof e.nodeType&&!function(){var t=r(e),o=t,i=getComputedStyle(e);e=[o.left,o.top,t.width+o.left,t.height+o.top],U.forEach(function(t,o){t=t[0].toUpperCase()+t.substr(1),"Top"===t||"Left"===t?e[o]+=parseFloat(i["border"+t+"Width"]):e[o]-=parseFloat(i["border"+t+"Width"])})}(),e}var w=function(){function t(t,e){for(var o=0;o1?a-1:0),h=1;a>h;h++)f[h-1]=arguments[h];i.apply(s,f),r?this.bindings[t].splice(e,1):++e}}}]),t}();C.Utils={getScrollParent:n,getBounds:r,getOffsetParent:s,extend:f,addClass:l,removeClass:h,hasClass:d,updateClasses:c,defer:T,flush:S,uniqueId:O,Evented:W,getScrollBarSize:a};var M=function(){function t(t,e){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}return function(e,o){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,o);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),w=function(){function t(t,e){for(var o=0;o16?(e=Math.min(e-16,250),void(o=setTimeout(n,250))):void("undefined"!=typeof t&&m()-t<10||("undefined"!=typeof o&&(clearTimeout(o),o=null),t=m(),_(),e=m()-t))};["resize","scroll","touchmove"].forEach(function(t){window.addEventListener(t,i)})}();var z={center:"center",left:"right",right:"left"},F={middle:"middle",top:"bottom",bottom:"top"},L={top:0,left:0,middle:"50%",center:"50%",bottom:"100%",right:"100%"},Y=function(t,e){var o=t.left,i=t.top;return"auto"===o&&(o=z[e.left]),"auto"===i&&(i=F[e.top]),{left:o,top:i}},H=function(t){var e=t.left,o=t.top;return"undefined"!=typeof L[t.left]&&(e=L[t.left]),"undefined"!=typeof L[t.top]&&(o=L[t.top]),{left:e,top:o}},X=function(t){var e=t.split(" "),o=M(e,2),i=o[0],n=o[1];return{top:i,left:n}},j=X,N=function(){function t(e){var o=this;i(this,t),this.position=this.position.bind(this),B.push(this),this.history=[],this.setOptions(e,!1),C.modules.forEach(function(t){"undefined"!=typeof t.initialize&&t.initialize.call(o)}),this.position()}return w(t,[{key:"getClass",value:function(){var t=void 0===arguments[0]?"":arguments[0],e=this.options.classes;return"undefined"!=typeof e&&e[t]?this.options.classes[t]:this.options.classPrefix?this.options.classPrefix+"-"+t:t}},{key:"setOptions",value:function(t){var e=this,o=void 0===arguments[1]?!0:arguments[1],i={offset:"0 0",targetOffset:"0 0",targetAttachment:"auto auto",classPrefix:"tether"};this.options=f(i,t);var r=this.options,s=r.element,a=r.target,h=r.targetModifier;if(this.element=s,this.target=a,this.targetModifier=h,"viewport"===this.target?(this.target=document.body,this.targetModifier="visible"):"scroll-handle"===this.target&&(this.target=document.body,this.targetModifier="scroll-handle"),["element","target"].forEach(function(t){if("undefined"==typeof e[t])throw new Error("Tether Error: Both element and target must be defined");"undefined"!=typeof e[t].jquery?e[t]=e[t][0]:"string"==typeof e[t]&&(e[t]=document.querySelector(e[t]))}),l(this.element,this.getClass("element")),this.options.addTargetClasses!==!1&&l(this.target,this.getClass("target")),!this.options.attachment)throw new Error("Tether Error: You must provide an attachment");this.targetAttachment=j(this.options.targetAttachment),this.attachment=j(this.options.attachment),this.offset=X(this.options.offset),this.targetOffset=X(this.options.targetOffset),"undefined"!=typeof this.scrollParent&&this.disable(),this.scrollParent="scroll-handle"===this.targetModifier?this.target:n(this.target),this.options.enabled!==!1&&this.enable(o)}},{key:"getTargetBounds",value:function(){if("undefined"==typeof this.targetModifier)return r(this.target);if("visible"===this.targetModifier){if(this.target===document.body)return{top:pageYOffset,left:pageXOffset,height:innerHeight,width:innerWidth};var t=r(this.target),e={height:t.height,width:t.width,top:t.top,left:t.left};return e.height=Math.min(e.height,t.height-(pageYOffset-t.top)),e.height=Math.min(e.height,t.height-(t.top+t.height-(pageYOffset+innerHeight))),e.height=Math.min(innerHeight,e.height),e.height-=2,e.width=Math.min(e.width,t.width-(pageXOffset-t.left)),e.width=Math.min(e.width,t.width-(t.left+t.width-(pageXOffset+innerWidth))),e.width=Math.min(innerWidth,e.width),e.width-=2,e.topo.clientWidth||[i.overflow,i.overflowX].indexOf("scroll")>=0||this.target!==document.body,s=0;n&&(s=15);var a=t.height-parseFloat(i.borderTopWidth)-parseFloat(i.borderBottomWidth)-s,e={width:15,height:.975*a*(a/o.scrollHeight),left:t.left+t.width-parseFloat(i.borderLeftWidth)-15},f=0;408>a&&this.target===document.body&&(f=-11e-5*Math.pow(a,2)-.00727*a+22.58),this.target!==document.body&&(e.height=Math.max(e.height,24));var h=this.target.scrollTop/(o.scrollHeight-a);return e.top=h*(a-e.height-f)+t.top+parseFloat(i.borderTopWidth),this.target===document.body&&(e.height=Math.max(e.height,24)),e}}},{key:"clearCache",value:function(){this._cache={}}},{key:"cache",value:function(t,e){return"undefined"==typeof this._cache&&(this._cache={}),"undefined"==typeof this._cache[t]&&(this._cache[t]=e.call(this)),this._cache[t]}},{key:"enable",value:function(){var t=void 0===arguments[0]?!0:arguments[0];this.options.addTargetClasses!==!1&&l(this.target,this.getClass("enabled")),l(this.element,this.getClass("enabled")),this.enabled=!0,this.scrollParent!==document&&this.scrollParent.addEventListener("scroll",this.position),t&&this.position()}},{key:"disable",value:function(){h(this.target,this.getClass("enabled")),h(this.element,this.getClass("enabled")),this.enabled=!1,"undefined"!=typeof this.scrollParent&&this.scrollParent.removeEventListener("scroll",this.position)}},{key:"destroy",value:function(){var t=this;this.disable(),B.forEach(function(e,o){return e===t?void B.splice(o,1):void 0})}},{key:"updateAttachClasses",value:function(t,e){var o=this;t=t||this.attachment,e=e||this.targetAttachment;var i=["left","top","bottom","right","middle","center"];"undefined"!=typeof this._addAttachClasses&&this._addAttachClasses.length&&this._addAttachClasses.splice(0,this._addAttachClasses.length),"undefined"==typeof this._addAttachClasses&&(this._addAttachClasses=[]);var n=this._addAttachClasses;t.top&&n.push(this.getClass("element-attached")+"-"+t.top),t.left&&n.push(this.getClass("element-attached")+"-"+t.left),e.top&&n.push(this.getClass("target-attached")+"-"+e.top),e.left&&n.push(this.getClass("target-attached")+"-"+e.left);var r=[];i.forEach(function(t){r.push(o.getClass("element-attached")+"-"+t),r.push(o.getClass("target-attached")+"-"+t)}),T(function(){"undefined"!=typeof o._addAttachClasses&&(c(o.element,o._addAttachClasses,r),o.options.addTargetClasses!==!1&&c(o.target,o._addAttachClasses,r),delete o._addAttachClasses)})}},{key:"position",value:function(){var t=this,e=void 0===arguments[0]?!0:arguments[0];if(this.enabled){this.clearCache();var o=Y(this.targetAttachment,this.attachment);this.updateAttachClasses(this.attachment,o);var i=this.cache("element-bounds",function(){return r(t.element)}),n=i.width,f=i.height;if(0===n&&0===f&&"undefined"!=typeof this.lastSize){var h=this.lastSize;n=h.width,f=h.height}else this.lastSize={width:n,height:f};var l=this.cache("target-bounds",function(){return t.getTargetBounds()}),d=l,u=y(H(this.attachment),{width:n,height:f}),p=y(H(o),d),c=y(this.offset,{width:n,height:f}),g=y(this.targetOffset,d);u=v(u,c),p=v(p,g);for(var m=l.left+p.left-u.left,b=l.top+p.top-u.top,w=0;wwindow.innerWidth&&(A=this.cache("scrollbar-size",a),x.viewport.bottom-=A.height),document.body.scrollHeight>window.innerHeight&&(A=this.cache("scrollbar-size",a),x.viewport.right-=A.width),(-1===["","static"].indexOf(document.body.style.position)||-1===["","static"].indexOf(document.body.parentElement.style.position))&&(x.page.bottom=document.body.scrollHeight-b-f,x.page.right=document.body.scrollWidth-m-n),"undefined"!=typeof this.options.optimizations&&this.options.optimizations.moveElement!==!1&&"undefined"==typeof this.targetModifier&&!function(){var e=t.cache("target-offsetparent",function(){return s(t.target)}),o=t.cache("target-offsetparent-bounds",function(){return r(e)}),i=getComputedStyle(e),n=o,a={};if(["Top","Left","Bottom","Right"].forEach(function(t){a[t.toLowerCase()]=parseFloat(i["border"+t+"Width"])}),o.right=document.body.scrollWidth-o.left-n.width+a.right,o.bottom=document.body.scrollHeight-o.top-n.height+a.bottom,x.page.top>=o.top+a.top&&x.page.bottom>=o.bottom&&x.page.left>=o.left+a.left&&x.page.right>=o.right){var f=e.scrollTop,h=e.scrollLeft;x.offset={top:x.page.top-o.top+f-a.top,left:x.page.left-o.left+h-a.left}}}(),this.move(x),this.history.unshift(x),this.history.length>3&&this.history.pop(),e&&S(),!0}}},{key:"move",value:function(t){var e=this;if("undefined"!=typeof this.element.parentNode){var o={};for(var i in t){o[i]={};for(var n in t[i]){for(var r=!1,a=0;a=0&&(v=parseFloat(v),m=parseFloat(m)),v!==m&&(c=!0,p[n]=m)}c&&T(function(){f(e.element.style,p)})}}}]),t}();N.modules=[],C.position=_;var R=f(N,C),M=function(){function t(t,e){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}return function(e,o){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,o);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),P=C.Utils,r=P.getBounds,f=P.extend,c=P.updateClasses,T=P.defer,U=["left","top","right","bottom"];C.modules.push({position:function(t){var e=this,o=t.top,i=t.left,n=t.targetAttachment;if(!this.options.constraints)return!0;var s=this.cache("element-bounds",function(){return r(e.element)}),a=s.height,h=s.width;if(0===h&&0===a&&"undefined"!=typeof this.lastSize){var l=this.lastSize;h=l.width,a=l.height}var d=this.cache("target-bounds",function(){return e.getTargetBounds()}),u=d.height,p=d.width,g=[this.getClass("pinned"),this.getClass("out-of-bounds")];this.options.constraints.forEach(function(t){var e=t.outOfBoundsClass,o=t.pinnedClass;e&&g.push(e),o&&g.push(o)}),g.forEach(function(t){["left","top","right","bottom"].forEach(function(e){g.push(t+"-"+e)})});var m=[],v=f({},n),y=f({},this.attachment);return this.options.constraints.forEach(function(t){var r=t.to,s=t.attachment,f=t.pin;"undefined"==typeof s&&(s="");var l=void 0,d=void 0;if(s.indexOf(" ")>=0){var c=s.split(" "),g=M(c,2);d=g[0],l=g[1]}else l=d=s;var w=b(e,r);("target"===d||"both"===d)&&(ow[3]&&"bottom"===v.top&&(o-=u,v.top="top")),"together"===d&&(ow[3]&&"bottom"===v.top&&("top"===y.top?(o-=u,v.top="top",o-=a,y.top="bottom"):"bottom"===y.top&&(o-=u,v.top="top",o+=a,y.top="top")),"middle"===v.top&&(o+a>w[3]&&"top"===y.top?(o-=a,y.top="bottom"):ow[2]&&"right"===v.left&&(i-=p,v.left="left")),"together"===l&&(iw[2]&&"right"===v.left?"left"===y.left?(i-=p,v.left="left",i-=h,y.left="right"):"right"===y.left&&(i-=p,v.left="left",i+=h,y.left="left"):"center"===v.left&&(i+h>w[2]&&"left"===y.left?(i-=h,y.left="right"):iw[3]&&"top"===y.top&&(o-=a,y.top="bottom")),("element"===l||"both"===l)&&(iw[2]&&"left"===y.left&&(i-=h,y.left="right")),"string"==typeof f?f=f.split(",").map(function(t){return t.trim()}):f===!0&&(f=["top","left","right","bottom"]),f=f||[];var C=[],O=[];o=0?(o=w[1],C.push("top")):O.push("top")),o+a>w[3]&&(f.indexOf("bottom")>=0?(o=w[3]-a,C.push("bottom")):O.push("bottom")),i=0?(i=w[0],C.push("left")):O.push("left")),i+h>w[2]&&(f.indexOf("right")>=0?(i=w[2]-h,C.push("right")):O.push("right")),C.length&&!function(){var t=void 0;t="undefined"!=typeof e.options.pinnedClass?e.options.pinnedClass:e.getClass("pinned"),m.push(t),C.forEach(function(e){m.push(t+"-"+e)})}(),O.length&&!function(){var t=void 0;t="undefined"!=typeof e.options.outOfBoundsClass?e.options.outOfBoundsClass:e.getClass("out-of-bounds"),m.push(t),O.forEach(function(e){m.push(t+"-"+e)})}(),(C.indexOf("left")>=0||C.indexOf("right")>=0)&&(y.left=v.left=!1),(C.indexOf("top")>=0||C.indexOf("bottom")>=0)&&(y.top=v.top=!1),(v.top!==n.top||v.left!==n.left||y.top!==e.attachment.top||y.left!==e.attachment.left)&&e.updateAttachClasses(y,v)}),T(function(){e.options.addTargetClasses!==!1&&c(e.target,m,g),c(e.element,m,g)}),{top:o,left:i}}});var P=C.Utils,r=P.getBounds,c=P.updateClasses,T=P.defer;C.modules.push({position:function(t){var e=this,o=t.top,i=t.left,n=this.cache("element-bounds",function(){return r(e.element)}),s=n.height,a=n.width,f=this.getTargetBounds(),h=o+s,l=i+a,d=[];o<=f.bottom&&h>=f.top&&["left","right"].forEach(function(t){var e=f[t];(e===i||e===l)&&d.push(t)}),i<=f.right&&l>=f.left&&["top","bottom"].forEach(function(t){var e=f[t];(e===o||e===h)&&d.push(t)});var u=[],p=[],g=["left","top","right","bottom"];return u.push(this.getClass("abutted")),g.forEach(function(t){u.push(e.getClass("abutted")+"-"+t)}),d.length&&p.push(this.getClass("abutted")),d.forEach(function(t){p.push(e.getClass("abutted")+"-"+t)}),T(function(){e.options.addTargetClasses!==!1&&c(e.target,p,u),c(e.element,p,u)}),!0}});var M=function(){function t(t,e){var o=[],i=!0,n=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done)&&(o.push(s.value),!e||o.length!==e);i=!0);}catch(f){n=!0,r=f}finally{try{!i&&a["return"]&&a["return"]()}finally{if(n)throw r}}return o}return function(e,o){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,o);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}();return C.modules.push({position:function(t){var e=t.top,o=t.left;if(this.options.shift){var i=this.options.shift;"function"==typeof this.options.shift&&(i=this.options.shift.call(this,{top:e,left:o}));var n=void 0,r=void 0;if("string"==typeof i){i=i.split(" "),i[1]=i[1]||i[0];var s=M(i,2);n=s[0],r=s[1],n=parseFloat(n,10),r=parseFloat(r,10)}else n=i.top,r=i.left;return e+=n,o+=r,{top:e,left:o}}}}),R}); H5P.Tether = Tether; window.Tether = oldTether; ; var oldDrop = window.Drop; var oldTether = window.Tether; Tether = H5P.Tether; !function(t,e){"function"==typeof define&&define.amd?define(["tether"],e):"object"==typeof exports?module.exports=e(require("tether")):t.Drop=e(t.Tether)}(this,function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function o(t){var e=t.split(" "),n=a(e,2),o=n[0],i=n[1];if(["left","right"].indexOf(o)>=0){var s=[i,o];o=s[0],i=s[1]}return[o,i].join(" ")}function i(t,e){for(var n=void 0,o=[];-1!==(n=t.indexOf(e));)o.push(t.splice(n,1));return o}function s(){var a=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],u=function(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];return new(r.apply(b,[null].concat(e)))};p(u,{createContext:s,drops:[],defaults:{}});var g={classPrefix:"drop",defaults:{position:"bottom left",openOn:"click",beforeClose:null,constrainToScrollParent:!0,constrainToWindow:!0,classes:"",remove:!1,tetherOptions:{}}};p(u,g,a),p(u.defaults,g.defaults,a.defaults),"undefined"==typeof x[u.classPrefix]&&(x[u.classPrefix]=[]),u.updateBodyClasses=function(){for(var t=!1,e=x[u.classPrefix],n=e.length,o=0;n>o;++o)if(e[o].isOpened()){t=!0;break}t?d(document.body,u.classPrefix+"-open"):c(document.body,u.classPrefix+"-open")};var b=function(s){function r(t){if(e(this,r),l(Object.getPrototypeOf(r.prototype),"constructor",this).call(this),this.options=p({},u.defaults,t),this.target=this.options.target,"undefined"==typeof this.target)throw new Error("Drop Error: You must provide a target.");var n="data-"+u.classPrefix,o=this.target.getAttribute(n);o&&(this.options.content=o);for(var i=["position","openOn"],s=0;s=0)for(var n=function(e){t.toggle(e),e.preventDefault()},o=function(e){t.isOpened()&&(e.target===t.drop||t.drop.contains(e.target)||e.target===t.target||t.target.contains(e.target)||t.close(e))},i=0;i=0&&(this._on(this.target,"mouseover",h),this._on(this.drop,"mouseover",h),this._on(this.target,"mouseout",l),this._on(this.drop,"mouseout",l)),e.indexOf("focus")>=0&&(this._on(this.target,"focus",h),this._on(this.drop,"focus",h),this._on(this.target,"blur",l),this._on(this.drop,"blur",l))}}},{key:"isOpened",value:function(){return this.drop?f(this.drop,u.classPrefix+"-open"):void 0}},{key:"toggle",value:function(t){this.isOpened()?this.close(t):this.open(t)}},{key:"open",value:function(t){var e=this;this.isOpened()||(this.drop.parentNode||document.body.appendChild(this.drop),"undefined"!=typeof this.tether&&this.tether.enable(),d(this.drop,u.classPrefix+"-open"),d(this.drop,u.classPrefix+"-open-transitionend"),setTimeout(function(){e.drop&&d(e.drop,u.classPrefix+"-after-open")}),"undefined"!=typeof this.tether&&this.tether.position(),this.trigger("open"),u.updateBodyClasses())}},{key:"_transitionEndHandler",value:function(t){t.target===t.currentTarget&&(f(this.drop,u.classPrefix+"-open")||c(this.drop,u.classPrefix+"-open-transitionend"),this.drop.removeEventListener(m,this.transitionEndHandler))}},{key:"beforeCloseHandler",value:function(t){var e=!0;return this.isClosing||"function"!=typeof this.options.beforeClose||(this.isClosing=!0,e=this.options.beforeClose(t,this)!==!1),this.isClosing=!1,e}},{key:"close",value:function(t){this.isOpened()&&this.beforeCloseHandler(t)&&(c(this.drop,u.classPrefix+"-open"),c(this.drop,u.classPrefix+"-after-open"),this.drop.addEventListener(m,this.transitionEndHandler),this.trigger("close"),"undefined"!=typeof this.tether&&this.tether.disable(),u.updateBodyClasses(),this.options.remove&&this.remove(t))}},{key:"remove",value:function(t){this.close(t),this.drop.parentNode&&this.drop.parentNode.removeChild(this.drop)}},{key:"position",value:function(){this.isOpened()&&"undefined"!=typeof this.tether&&this.tether.position()}},{key:"destroy",value:function(){this.remove(),"undefined"!=typeof this.tether&&this.tether.destroy();for(var t=0;t= 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 ($) { /** * 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 JoubelHelpTextDialog(header, message) { var $helpTextDialogBox = $('
', { 'class': 'joubel-help-text-dialog-box' }); var $helpTextDialogBackground = $('
', { 'class': 'joubel-help-text-dialog-background' }).appendTo($helpTextDialogBox); var $helpTextDialogContainer = $('
', { 'class': 'joubel-help-text-dialog-container' }).appendTo($helpTextDialogBox); $('
', { 'class': 'joubel-help-text-header', 'html': header }).appendTo($helpTextDialogContainer); $('
', { 'class': 'joubel-help-text-body', 'html': message }).appendTo($helpTextDialogContainer); $('
', { 'class': 'joubel-help-text-remove', 'tabindex': 0 }).click(function () { $helpTextDialogBox.remove(); }).keydown(function (e) { var keyPressed = e.which; // 32 - space if (keyPressed === 32) { $(this).click(); e.preventDefault(); } }).appendTo($helpTextDialogContainer); return $helpTextDialogBox; } return JoubelHelpTextDialog; }(H5P.jQuery)); ; var H5P = H5P || {}; /** * Class responsible for creating auto-disappearing dialogs */ H5P.JoubelMessageDialog = (function ($) { /** * Display a pop-up containing a message. * * @param {H5P.jQuery} $container The container which message dialog will be appended to * @param {string} message The message * @return {H5P.jQuery} */ function JoubelMessageDialog ($container, message) { var timeout; var removeDialog = function () { $warning.remove(); clearTimeout(timeout); $container.off('click.messageDialog'); }; // Create warning popup: var $warning = $('
', { 'class': 'joubel-message-dialog', text: message }).appendTo($container); // Remove after 3 seconds or if user clicks anywhere in $container: timeout = setTimeout(removeDialog, 3000); $container.on('click.messageDialog', removeDialog); return $warning; } return JoubelMessageDialog; })(H5P.jQuery); ; var H5P = H5P || {}; /** * Class responsible for creating a circular progress bar */ H5P.JoubelProgressCircle = (function ($) { /** * Constructor for the Progress Circle * * @param {Number} number The amount of progress to display * @param {string} progressColor Color for the progress meter * @param {string} backgroundColor Color behind the progress meter */ function ProgressCircle(number, progressColor, fillColor, backgroundColor) { progressColor = progressColor || '#096bcb'; 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) { console.log('Progress circle input' + e); number = 'err'; } //Draw circle if (number > 100) { number = 100; } var decimalNumber = 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(this.rgbFromAlpha(progressColorRGB.r, decimalNumber), 10) + ',' + parseInt(this.rgbFromAlpha(progressColorRGB.g, decimalNumber), 10) + ',' + parseInt(this.rgbFromAlpha(progressColorRGB.b, decimalNumber), 10) + ')'; // Circle wrapper var $wrapper = $('
', { 'class': "joubel-progress-circle-wrapper" }); //Active border indicates progress var $activeBorder = $('
', { 'class': "joubel-progress-circle-active-border" }).appendTo($wrapper); //Background circle var $backgroundCircle = $('
', { 'class': "joubel-progress-circle-circle" }).appendTo($activeBorder); //Progress text/number $('', { 'text': number, 'class': "joubel-progress-circle-percentage" }).appendTo($backgroundCircle); var deg = number * 3.6; if (deg <= 180) { $activeBorder.css('background-image', 'linear-gradient(' + (90 + deg) + 'deg, transparent 50%, ' + fillColor + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } else { $activeBorder.css('background-image', 'linear-gradient(' + (deg - 90) + 'deg, transparent 50%, ' + progressColorString + ' 50%),' + 'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)') .css('border', '2px solid' + backgroundColor) .css('background-color', progressColorString); } this.$activeBorder = $activeBorder; this.$backgroundCircle = $backgroundCircle; this.$wrapper = $wrapper; this.initResizeFunctionality(); return $wrapper; } /** * Initializes resize functionality for the progress circle */ ProgressCircle.prototype.initResizeFunctionality = function () { var self = this; $(window).resize(function () { // Queue resize setTimeout(function () { self.resize(); }); }); // First resize setTimeout(function () { self.resize(); }, 0); }; /** * Resize function makes progress circle grow or shrink relative to parent container */ ProgressCircle.prototype.resize = function () { var $parent = this.$wrapper.parent(); if ($parent !== undefined && $parent) { // Measurements var fontSize = parseInt($parent.css('font-size'), 10); // Static sizes var fontSizeMultiplum = 3.75; var progressCircleWidthPx = parseInt((fontSize / 4.5), 10) % 2 === 0 ? parseInt((fontSize / 4.5), 10) : parseInt((fontSize / 4.5), 10) + 1; 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; }; /** * Convert rgb and opacity to new rgb * @param {number} colorValue * @param {float} opacity * @returns {number} blended colorValue */ ProgressCircle.prototype.rgbFromAlpha = function (colorValue, opacity) { return (opacity * colorValue) + (1 - opacity) * 255; }; return ProgressCircle; }(H5P.jQuery)); ; var H5P = H5P || {}; H5P.SimpleRoundedButton = (function ($) { /** * Creates a new tip */ function SimpleRoundedButton(text) { var $simpleRoundedButton = $('
', { 'class': 'joubel-simple-rounded-button', 'title': text, 'role': 'button', 'tabindex': '0' }).keydown(function (e) { var keyPressed = e.which; // 32 - space if (keyPressed === 32) { $(this).click(); e.preventDefault(); } $(this).focus(); }); $('', { '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 removeSpeechBubbleTimeout; 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; $currentContainer = $container; this.isCurrent = function ($tip) { return $tip === $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 = $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'); } // Make sure we fade out old speech bubble fadeOutSpeechBubble($currentSpeechBubble); // Create bubble var $tail = $('
'); var $innerTail = $('
'); var $innerBubble = $( '
' + '
' + text + '
' + '
' ).prepend($innerTail); $currentSpeechBubble = $( '
' ).append([$tail, $innerBubble]) .appendTo($h5pContainer); // Show speech bubble with transition setTimeout(function () { $currentSpeechBubble.addClass('show'); }, 0); // 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); // Handle click to close H5P.$body.on('mousedown.speechBubble', handleOutsideClick); // 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(); } }); /** * Static function for removing the speechbubble */ var remove = function() { H5P.$body.off('mousedown.speechBubble'); $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 with a fade * * @param {jQuery} $speechBubble Speech bubble element */ function fadeOutSpeechBubble($speechBubble) { if (!$speechBubble) {return;} // Stop removing bubble clearTimeout(removeSpeechBubbleTimeout); $speechBubble.removeClass('show'); setTimeout(function () { if ($speechBubble) { $speechBubble.remove(); $speechBubble = undefined; } }, 500); } /** * 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' }; } else { return { width: width + 'px', top: position.top + 'px', left: position.left + 'px', fontSize: fontSize + 'px' }; } } /** * 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' }; } else { return { top: position.top + 'px', left: position.left + 'px' }; } } /** * Calculates the offset between an element inside a container and the * container. Only works if all the edges of the inner element is inside the * outer element. * Width/height of the elements is included as a convenience. * * @param {H5P.jQuery} $outer * @param {H5P.jQuery} $inner * @return {object} Position offset */ function getOffsetBetween($outer, $inner) { var outer = $outer[0].getBoundingClientRect(); var inner = $inner[0].getBoundingClientRect(); return { top: inner.top - outer.top, right: outer.right - inner.right, bottom: outer.bottom - inner.bottom, left: inner.left - outer.left, innerWidth: inner.width, innerHeight: inner.height, outerWidth: outer.width, outerHeight: outer.height }; } return JoubelSpeechBubble; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelThrobber = (function ($) { /** * Creates a new tip */ function JoubelThrobber() { // h5p-throbber css is described in core var $throbber = $('
', { 'class': 'h5p-throbber' }); return $throbber; } return JoubelThrobber; }(H5P.jQuery)); ; var H5P = H5P || {}; H5P.JoubelTip = (function ($) { /** * Creates a new tip * * @param {string} text The text to display in the popup * @param {object} params Additional parameters * @param {string} [params.tipLabel] Tip label */ function JoubelTip(text, params) { var speechBubble; params = $.extend({ showSpeechBubble: true, helpIcon: false }, params); var parsedTitle = text; if ($.parseHTML($.trim(text)).length) { parsedTitle = $.parseHTML($.trim(text))[0].textContent; } var $tip = $('
', { class: 'joubel-tip-container' + (params.showSpeechBubble ? '' : ' be-quiet'), title: parsedTitle, role: 'button', tabindex: 0, click: function () { toggleSpeechBubble(); return false; }, keydown: function (e) { // Space if (e.which === 32) { toggleSpeechBubble(); e.preventDefault(); } else { toggleSpeechBubble(true); } } }).append($('
', { 'class': 'joubel-tip-icon' + (params.helpIcon ? ' help-icon': '') })); /** * Add or remove a speech bubble * @private * @param {boolean} [close] Forces tip close * @return {boolean} */ function toggleSpeechBubble(close) { if (speechBubble !== undefined && speechBubble.isCurrent($tip)) { speechBubble.remove(); speechBubble = undefined; } else if (!close && params.showSpeechBubble) { speechBubble = H5P.JoubelSpeechBubble($tip, text); } } return $tip; } 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); var self = this; this.$slider = $('
', $.extend({ 'class': 'h5p-joubel-ui-slider' }, params)); this.$slides = []; this.currentIndex = 0; this.numSlides = 0; } JoubelSlider.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelSlider.prototype.constructor = JoubelSlider; JoubelSlider.prototype.addSlide = function ($content) { $content.addClass('h5p-joubel-ui-slide').css({ 'left': (this.numSlides*100) + '%' }); this.$slider.append($content); this.$slides.push($content); this.numSlides++; if(this.numSlides === 1) { $content.addClass('current'); } }; JoubelSlider.prototype.attach = function ($container) { $container.append(this.$slider); }; JoubelSlider.prototype.move = function (index) { var self = this; if(index === 0) { self.trigger('first-slide'); } if(index+1 === self.numSlides) { self.trigger('last-slide'); } self.trigger('move'); var $previousSlide = self.$slides[this.currentIndex]; H5P.Transition.onTransitionEnd(this.$slider, function () { $previousSlide.removeClass('current'); self.trigger('moved'); }); this.$slides[index].addClass('current'); var translateX = 'translateX(' + (-index*100) + '%)'; this.$slider.css({ '-webkit-transform': translateX, '-moz-transform': translateX, '-ms-transform': translateX, 'transform': translateX }); this.currentIndex = index; }; JoubelSlider.prototype.remove = function () { this.$slider.remove(); }; JoubelSlider.prototype.next = function () { if(this.currentIndex+1 >= this.numSlides) { return; } this.move(this.currentIndex+1); }; JoubelSlider.prototype.previous = function () { this.move(this.currentIndex-1); }; JoubelSlider.prototype.first = function () { this.move(0); }; JoubelSlider.prototype.last = function () { this.move(this.numSlides-1); }; return JoubelSlider; })(H5P.jQuery); ; var H5P = H5P || {}; /** * @module */ H5P.JoubelScoreBar = (function ($) { /** * Creates a score bar * @class H5P.JoubelScoreBar * @param {number=} maxScore Maximum score * @param {string} [label] Makes it easier for readspeakers to identify the scorebar */ function JoubelScoreBar(maxScore, label) { var self = this; self.maxScore = maxScore; self.score = 0; /** * @method hasFullScore * @private * @return {Boolean} true if full score, else false */ var hasFullScore = function () { return self.score === self.maxScore; }; /** * @function appendTo * @memberOf H5P.JoubelScoreBar# * @param {H5P.jQuery} $wrapper Dom container */ self.appendTo = function ($wrapper) { self.$scoreBar.appendTo($wrapper); }; /** * Creates the html for this widget * * @method createHtml * @private */ var createHtml = function () { // Container div self.$scoreBar = $('
', { 'class': 'h5p-joubelui-score-bar', 'role': 'progressbar', 'aria-valuenow': 0, 'aria-valuemin': 0, 'aria-valuemax': self.maxScore }); if (label) { self.$scoreBar.attr('aria-label', label + '.'); } // The progress bar wrapper self.$progressWrapper = $('
', { 'class': 'h5p-joubelui-score-bar-progress-wrapper' }).appendTo(self.$scoreBar); self.$progress = $('
', { 'class': 'h5p-joubelui-score-bar-progress' }).appendTo(self.$progressWrapper); // The star self.$endWrapper = $('
', { 'class': 'h5p-joubelui-score-bar-end' }).appendTo(self.$scoreBar); // The default star self.$defaultStar = $('', { 'class': 'h5p-joubelui-score-bar-default-star' }).appendTo(self.$endWrapper); // The full score star self.$fullScoreStar = $('', { 'class': 'h5p-joubelui-score-bar-full-score-star' }).appendTo(self.$endWrapper); }; /** * 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 () { var fullscore = hasFullScore(); self.$scoreBar.attr('aria-valuenow', self.score); setTimeout(function () { self.$progress.addClass('animate'); self.$progress.css({ width: (fullscore ? '102' : (self.maxScore - 1 !== 0 ? (self.score * 100 / (self.maxScore - 1)) : 0)) + '%' }); H5P.Transition.sequence([ { $element: self.$progress, timeout: 600, end: function () { self.$progress.removeClass('animate'); self.$scoreBar.toggleClass('full-score', fullscore); if (fullscore) { self.$fullScoreStar.addClass('animate-background'); } }, break: !fullscore }, { $element: self.$fullScoreStar, timeout: 400, end: function () { self.$fullScoreStar.addClass('animate-star show-star'); } }, { $element: self.$fullScoreStar, end: function () { self.$fullScoreStar.removeClass('animate-star'); self.$fullScoreStar.addClass('animate-star-blink'); } } ]); }, 300); }; /** * Removes all classes * @method reset */ self.reset = function () { self.$fullScoreStar.removeClass('animate-star animate-star-blink show-star animate-background'); self.$scoreBar.removeClass('full-score'); }; createHtml(); } return JoubelScoreBar; })(H5P.jQuery); ; var H5P = H5P || {}; H5P.JoubelProgressbar = (function ($) { /** * Joubel progressbar class * @method JoubelProgressbar * @constructor * @param {number} steps Number of steps * @param {Object} [options] Additional options * @param {boolean} [options.disableAria] Disable readspeaker assistance * @param {string} [options.progressText] A progress text for describing * current progress out of total progress for readspeakers. * e.g. "Slide :num of :total" */ function JoubelProgressbar(steps, options) { H5P.EventDispatcher.call(this); var self = this; this.options = $.extend({ progressText: 'Slide :num of :total' }, options); this.currentStep = 0; this.steps = steps; this.$progressbar = $('
', { 'class': 'h5p-joubelui-progressbar', on: { click: function () { self.toggleTooltip(); return false; }, mouseenter: function () { self.showTooltip(); }, mouseleave: function () { setTimeout(function () { self.hideTooltip(); }, 1500) } } }); this.$background = $('
', { 'class': 'h5p-joubelui-progressbar-background' }).appendTo(this.$progressbar); $('body').click(function () { self.toggleTooltip(true); }); } JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype); JoubelProgressbar.prototype.constructor = JoubelProgressbar; /** * Display tooltip * @method showTooltip */ JoubelProgressbar.prototype.showTooltip = function () { var self = this; if (this.currentStep === 0 || this.tooltip !== undefined) { return; } var parentWidth = self.$progressbar.offset().left + self.$progressbar.width(); this.tooltip = new H5P.Drop({ target: this.$background.get(0), content: this.currentStep + '/' + this.steps, classes: 'drop-theme-arrows-bounce h5p-joubelui-drop', position: 'top right', openOn: 'always', tetherOptions: { attachment: 'bottom center', targetAttachment: 'top right' } }); this.tooltip.on('open', function () { var $drop = $(self.tooltip.drop); var left = $drop.position().left; var dropWidth = $drop.width(); // Need to handle drops getting outside of the progressbar: if (left < 0) { $drop.css({marginLeft: (-left) + 'px'}); } else if (left + dropWidth > parentWidth) { $drop.css({marginLeft: (parentWidth - (left + dropWidth)) + 'px'}); } }); }; JoubelProgressbar.prototype.updateAria = function () { var self = this; if (this.options.disableAria) { return; } if (!this.$currentStatus) { this.$currentStatus = $('
', { 'class': 'h5p-joubelui-progressbar-slide-status-text', 'aria-live': 'assertive' }).appendTo(this.$progressbar); } var interpolatedProgressText = self.options.progressText .replace(':num', self.currentStep) .replace(':total', self.steps); this.$currentStatus.html(interpolatedProgressText); }; /** * Hides tooltip * @method hideTooltip */ JoubelProgressbar.prototype.hideTooltip = function () { if (this.tooltip !== undefined) { this.tooltip.remove(); this.tooltip.destroy(); this.tooltip = undefined; } }; /** * Toggles tooltip-visibility * @method toggleTooltip * @param {boolean} [closeOnly] Don't show, only close if open */ JoubelProgressbar.prototype.toggleTooltip = function (closeOnly) { if (this.tooltip === undefined && !closeOnly) { this.showTooltip(); } else if (this.tooltip !== undefined) { this.hideTooltip(); } }; /** * Appends to a container * @method appendTo * @param {H5P.jquery} $container */ JoubelProgressbar.prototype.appendTo = function ($container) { this.$progressbar.appendTo($container); }; /** * Update progress * @method setProgress * @param {number} step */ JoubelProgressbar.prototype.setProgress = function (step) { // Check for valid value: if (step > this.steps || step < 0) { return; } this.currentStep = step; this.$background.css({ width: ((this.currentStep/this.steps)*100) + '%' }); this.updateAria(); }; /** * Increment progress with 1 * @method next */ JoubelProgressbar.prototype.next = function () { this.setProgress(this.currentStep+1); }; /** * Reset progressbar * @method reset */ JoubelProgressbar.prototype.reset = function () { this.setProgress(0); }; /** * Check if last step is reached * @method isLastStep * @return {Boolean} */ JoubelProgressbar.prototype.isLastStep = function () { return this.steps === this.currentStep; }; return JoubelProgressbar; })(H5P.jQuery); ; 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 * @return {H5P.JoubelHelpTextDialog} */ JoubelUI.createHelpTextDialog = function (header, message) { return new H5P.JoubelHelpTextDialog(header, message); }; /** * 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) { return new H5P.JoubelScoreBar(maxScore, label); }; /** * 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); }; 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', 'feedback', 'buttons', 'read']; // Keep track of registered sections var sections = {}; // Buttons var buttons = {}; var buttonOrder = []; // Wrapper when attached var $wrapper; // 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 = $('
', { '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; } } }; /** * 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; } // Get natural element height var $tmp = $element.clone() .css({ 'position': 'absolute', 'max-height': 'none' }).appendTo($element.parent()); // 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 sections.buttons.$element.addClass('h5p-question-visible'); // Resize buttons after resizing button section setTimeout(function () { 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 () { // 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; } } if (sections.buttons && numToHide === sections.buttons.$element.children().length) { // All buttons are going to be hidden. Hide container using transition. sections.buttons.$element.removeClass('h5p-question-visible'); sections.buttons.$element.css('max-height', ''); sectionsIsTransitioning = true; // Wait for animations before detaching buttons toggleButtonsTransitionTimer = setTimeout(function () { hideButtons(relocateFocus); sectionsIsTransitioning = false; }, 150); } else { hideButtons(relocateFocus); // Show button section if (!sections.buttons.$element.is(':empty')) { sections.buttons.$element.addClass('h5p-question-visible'); setElementHeight(sections.buttons.$element); // Trigger resize after animation toggleButtonsTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 150); } } // 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; } // Clear button truncation timer if within a button truncation function if (buttonTruncationTimer) { clearTimeout(buttonTruncationTimer); } // Allow button section to attach before getting width buttonTruncationTimer = setTimeout(function () { // A static margin is added as buffer for smoother transitions var buttonsWidth = 0; for (var i in buttons) { var $element = buttons[i].$element; if (buttons[i].isVisible) { //Calculate exact button width var buttonInstanceWidth = $element.get(0).offsetWidth + parseFloat($element.css('margin-left')) + parseFloat($element.css('margin-right')); buttonsWidth += Math.ceil(buttonInstanceWidth) + 1; } } // Button section reduced by 1 pixel for cross-broswer consistency. var buttonSectionWidth = Math.floor(sections.buttons.$element.get(0).offsetWidth) - 1; // Remove button labels if width of buttons are too wide if (buttonsWidth >= buttonSectionWidth) { removeButtonLabels(buttonsWidth, buttonSectionWidth); } else { restoreButtonLabels(buttonsWidth, buttonSectionWidth); } buttonTruncationTimer = undefined; }, 0); }; /** * 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]; if (!buttons[buttonId].isTruncated && buttons[buttonId].isVisible) { var $button = buttons[buttonId].$element; var $tmp = $button.clone() .css({ 'position': 'absolute', 'white-space': 'nowrap', 'max-width': 'none' }) .addClass('truncated') .html('') .appendTo($button.parent()); // Calculate new total width of buttons buttonsWidth = buttonsWidth - $button.outerWidth(true) + $tmp.outerWidth(true); // Remove label $button.html(''); $button.addClass('truncated'); buttons[buttonId].isTruncated = true; $tmp.remove(); 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]; if (buttons[buttonId].isTruncated && buttons[buttonId].isVisible) { // Check if adding label exceeds allowed width var $button = buttons[buttonId].$element; var $tmp = $button.clone() .css({ 'position': 'absolute', 'white-space': 'nowrap', 'max-width': 'none' }).removeClass('truncated') .html(buttons[buttonId].text) .appendTo($button.parent()); // Make sure clone was successfull if(!$button.length || !$tmp.length) { return; } var oldButtonSize = Math.floor($button.get(0).offsetWidth) - 1; var newButtonSize = Math.ceil($tmp.get(0).offsetWidth) + 1; // Calculate new total width of buttons with a static pixel for consistency cross-browser buttonsWidth = buttonsWidth - Math.floor(oldButtonSize) + Math.ceil(newButtonSize) + 1; $tmp.remove(); if (buttonsWidth >= maxButtonsWidth) { return; } // Restore label $button.html(buttons[buttonId].text); $button.removeClass('truncated'); buttons[buttonId].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; }; /** * 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: $('
', { 'class': 'h5p-question-video' }) }; if (disableAutoPlay) { params.params.autoplay = false; } // Never fit to wrapper params.params.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 {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 = $('
', { 'class': 'h5p-question-image h5p-question-image-fill-width' }); // Inner wrap var $imgWrap = $('
', { 'class': 'h5p-question-image-wrap', appendTo: sections.image.$element }); // Image element var $img = $('', { src: H5P.getPath(path, self.contentId), alt: (options.alt === undefined ? '' : options.alt), 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.load(function () { 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) { 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()); } }; /** * Set feedback message. * Setting the message to blank or undefined will hide it again. * * @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 */ self.setFeedback = function (content, score, maxScore, scoreBarLabel, helpText) { // Feedback is disabled if (behaviour.disableFeedback) { return self; } clearTimeout(feedbackTransitionTimer); if (content) { var $feedback = $('
', { 'class': 'h5p-question-feedback-container' }); if (scoreBar === undefined) { scoreBar = JoubelUI.createScoreBar(maxScore, scoreBarLabel); } scoreBar.appendTo($feedback); scoreBar.setScore(score); var $feedbackContent = $('
', { 'class': 'h5p-question-feedback-content' }).appendTo($feedback); // Feedback text $('
', { 'class': 'h5p-question-feedback-content-text', 'html': content }).appendTo($feedbackContent); if (helpText) { JoubelUI.createTip(helpText, {helpIcon: true}) .appendTo($feedbackContent); } // Feedback for readspeakers if (!behaviour.disableReadSpeaker) { self.read(content); } showFeedback = true; if (sections.feedback) { // Update section update('feedback', $feedback); } else { // Create section register('feedback', $feedback); if (initialized && $wrapper) { insert(self.order, 'feedback', sections, $wrapper); } } // Show feedback section feedbackTransitionTimer = setTimeout(function () { sections.feedback.$element.addClass('h5p-question-visible'); setElementHeight(sections.feedback.$element); sectionsIsTransitioning = true; // Scroll to bottom after showing feedback scrollToBottom(); // Trigger resize after animation feedbackTransitionTimer = setTimeout(function () { sectionsIsTransitioning = false; self.trigger('resize'); }, 150); }, 0); } else if (sections.feedback && showFeedback) { showFeedback = false; // Hide feedback section sections.feedback.$element.removeClass('h5p-question-visible'); sections.feedback.$element.css('max-height', ''); 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(); // Trigger resize after animation self.trigger('resize'); } sectionsIsTransitioning = false; scoreBar.setScore(0); }, 150); } 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); } 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 }; var $e = buttons[id].$element = JoubelUI.createButton($.extend({ 'class': 'h5p-question-' + id, html: text, on: { click: function (event) { handleButtonClick(); if (options.href !== undefined) { event.preventDefault(); } }, keydown: function (event) { switch (event.which) { case 13: // Enter case 32: // Space handleButtonClick(); event.preventDefault(); } } } }, options)); buttonOrder.push(id); if (visible === undefined || visible) { // Button should be visible $e.appendTo(sections.buttons.$element); buttons[id].isVisible = true; sections.buttons.$element.addClass('h5p-question-visible'); } return self; }; /** * 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) { if (buttons[id] === undefined) { 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) { if (buttons[id] === undefined) { 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++) { if (buttons[buttonOrder[i]].isVisible) { // Give that button focus buttons[buttonOrder[i]].$element.focus(); break; } } } else if (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 = $('
', { '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); } resizeButtons(); }); } // Inheritance Question.prototype = Object.create(EventDispatcher.prototype); Question.prototype.constructor = Question; return Question; })(H5P.jQuery, H5P.EventDispatcher, H5P.JoubelUI); ; (function(){var rsplit=function(string,regex){var result=regex.exec(string),retArr=new Array(),first_idx,last_idx,first_bit;while(result!=null){first_idx=result.index;last_idx=regex.lastIndex;if((first_idx)!=0){first_bit=string.substring(0,first_idx);retArr.push(string.substring(0,first_idx));string=string.slice(first_idx)}retArr.push(result[0]);string=string.slice(result[0].length);result=regex.exec(string)}if(!string==""){retArr.push(string)}return retArr},chop=function(string){return string.substr(0,string.length-1)},extend=function(d,s){for(var n in s){if(s.hasOwnProperty(n)){d[n]=s[n]}}};EJS=function(options){options=typeof options=="string"?{view:options}:options;this.set_options(options);if(options.precompiled){this.template={};this.template.process=options.precompiled;EJS.update(this.name,this);return }if(options.element){if(typeof options.element=="string"){var name=options.element;options.element=document.getElementById(options.element);if(options.element==null){throw name+"does not exist!"}}if(options.element.value){this.text=options.element.value}else{this.text=options.element.innerHTML}this.name=options.element.id;this.type="["}else{if(options.url){options.url=EJS.endExt(options.url,this.extMatch);this.name=this.name?this.name:options.url;var url=options.url;var template=EJS.get(this.name,this.cache);if(template){return template}if(template==EJS.INVALID_PATH){return null}try{this.text=EJS.request(url+(this.cache?"":"?"+Math.random()))}catch(e){}if(this.text==null){throw ({type:"EJS",message:"There is no template at "+url})}}}var template=new EJS.Compiler(this.text,this.type);template.compile(options,this.name);EJS.update(this.name,this);this.template=template};EJS.prototype={render:function(object,extra_helpers){object=object||{};this._extra_helpers=extra_helpers;var v=new EJS.Helpers(object,extra_helpers||{});return this.template.process.call(object,object,v)},update:function(element,options){if(typeof element=="string"){element=document.getElementById(element)}if(options==null){_template=this;return function(object){EJS.prototype.update.call(_template,element,object)}}if(typeof options=="string"){params={};params.url=options;_template=this;params.onComplete=function(request){var object=eval(request.responseText);EJS.prototype.update.call(_template,element,object)};EJS.ajax_request(params)}else{element.innerHTML=this.render(options)}},out:function(){return this.template.out},set_options:function(options){this.type=options.type||EJS.type;this.cache=options.cache!=null?options.cache:EJS.cache;this.text=options.text||null;this.name=options.name||null;this.ext=options.ext||EJS.ext;this.extMatch=new RegExp(this.ext.replace(/\./,"."))}};EJS.endExt=function(path,match){if(!path){return null}match.lastIndex=0;return path+(match.test(path)?"":this.ext)};EJS.Scanner=function(source,left,right){extend(this,{left_delimiter:left+"%",right_delimiter:"%"+right,double_left:left+"%%",double_right:"%%"+right,left_equal:left+"%=",left_comment:left+"%#"});this.SplitRegexp=left=="["?/(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/:new RegExp("("+this.double_left+")|(%%"+this.double_right+")|("+this.left_equal+")|("+this.left_comment+")|("+this.left_delimiter+")|("+this.right_delimiter+"\n)|("+this.right_delimiter+")|(\n)");this.source=source;this.stag=null;this.lines=0};EJS.Scanner.to_text=function(input){if(input==null||input===undefined){return""}if(input instanceof Date){return input.toDateString()}if(input.toString){return input.toString()}return""};EJS.Scanner.prototype={scan:function(block){scanline=this.scanline;regex=this.SplitRegexp;if(!this.source==""){var source_split=rsplit(this.source,/\n/);for(var i=0;i0){for(var i=0;i0){buff.push(put_cmd+'"'+clean(content)+'")')}content="";break;case scanner.double_left:content=content+scanner.left_delimiter;break;default:content=content+token;break}}else{switch(token){case scanner.right_delimiter:switch(scanner.stag){case scanner.left_delimiter:if(content[content.length-1]=="\n"){content=chop(content);buff.push(content);buff.cr()}else{buff.push(content)}break;case scanner.left_equal:buff.push(insert_cmd+"(EJS.Scanner.to_text("+content+")))");break}scanner.stag=null;content="";break;case scanner.double_right:content=content+scanner.right_delimiter;break;default:content=content+token;break}}});if(content.length>0){buff.push(put_cmd+'"'+clean(content)+'")')}buff.close();this.out=buff.script+";";var to_be_evaled="/*"+name+"*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {"+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";try{eval(to_be_evaled)}catch(e){if(typeof JSLINT!="undefined"){JSLINT(this.out);for(var i=0;i").replace(/''/g,"'")}return""}};EJS.newRequest=function(){var factories=[function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new XMLHttpRequest()},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var i=0;i")};EJS.Helpers.prototype.start_tag_for=function(A,B){return this.tag(A,B)};EJS.Helpers.prototype.submit_tag=function(A,B){B=B||{};B.type=B.type||"submit";B.value=A||"Submit";return this.single_tag_for("input",B)};EJS.Helpers.prototype.tag=function(C,E,D){if(!D){var D=">"}var B=" ";for(var A in E){if(E[A]!=null){var F=E[A].toString()}else{var F=""}if(A=="Class"){A="class"}if(F.indexOf("'")!=-1){B+=A+'="'+F+'" '}else{B+=A+"='"+F+"' "}}return"<"+C+B+D};EJS.Helpers.prototype.tag_end=function(A){return""};EJS.Helpers.prototype.text_area_tag=function(A,C,B){B=B||{};B.id=B.id||A;B.name=B.name||A;C=C||"";if(B.size){B.cols=B.size.split("x")[0];B.rows=B.size.split("x")[1];delete B.size}B.cols=B.cols||50;B.rows=B.rows||4;return this.start_tag_for("textarea",B)+C+this.tag_end("textarea")};EJS.Helpers.prototype.text_tag=EJS.Helpers.prototype.text_area_tag;EJS.Helpers.prototype.text_field_tag=function(A,C,B){return this.input_field_tag(A,C,"text",B)};EJS.Helpers.prototype.url_for=function(A){return'window.location="'+A+'";'};EJS.Helpers.prototype.img_tag=function(B,C,A){A=A||{};A.src=B;A.alt=C;return this.single_tag_for("img",A)}; EJS.Helpers.prototype.date_tag = function(name, value , html_options) { if(! (value instanceof Date)) value = new Date() var month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var years = [], months = [], days =[]; var year = value.getFullYear(); var month = value.getMonth(); var day = value.getDate(); for(var y = year - 15; y < year+15 ; y++) { years.push({value: y, text: y}) } for(var m = 0; m < 12; m++) { months.push({value: (m), text: month_names[m]}) } for(var d = 0; d < 31; d++) { days.push({value: (d+1), text: (d+1)}) } var year_select = this.select_tag(name+'[year]', year, years, {id: name+'[year]'} ) var month_select = this.select_tag(name+'[month]', month, months, {id: name+'[month]'}) var day_select = this.select_tag(name+'[day]', day, days, {id: name+'[day]'}) return year_select+month_select+day_select; } EJS.Helpers.prototype.form_tag = function(action, html_options) { html_options = html_options || {}; html_options.action = action if(html_options.multipart == true) { html_options.method = 'post'; html_options.enctype = 'multipart/form-data'; } return this.start_tag_for('form', html_options) } EJS.Helpers.prototype.form_tag_end = function() { return this.tag_end('form'); } EJS.Helpers.prototype.hidden_field_tag = function(name, value, html_options) { return this.input_field_tag(name, value, 'hidden', html_options); } EJS.Helpers.prototype.input_field_tag = function(name, value , inputType, html_options) { html_options = html_options || {}; html_options.id = html_options.id || name; html_options.value = value || ''; html_options.type = inputType || 'text'; html_options.name = name; return this.single_tag_for('input', html_options) } EJS.Helpers.prototype.is_current_page = function(url) { return (window.location.href == url || window.location.pathname == url ? true : false); } EJS.Helpers.prototype.link_to = function(name, url, html_options) { if(!name) var name = 'null'; if(!html_options) var html_options = {} if(html_options.confirm){ html_options.onclick = " var ret_confirm = confirm(\""+html_options.confirm+"\"); if(!ret_confirm){ return false;} " html_options.confirm = null; } html_options.href=url return this.start_tag_for('a', html_options)+name+ this.tag_end('a'); } EJS.Helpers.prototype.submit_link_to = function(name, url, html_options){ if(!name) var name = 'null'; if(!html_options) var html_options = {} html_options.onclick = html_options.onclick || '' ; if(html_options.confirm){ html_options.onclick = " var ret_confirm = confirm(\""+html_options.confirm+"\"); if(!ret_confirm){ return false;} " html_options.confirm = null; } html_options.value = name; html_options.type = 'submit' html_options.onclick=html_options.onclick+ (url ? this.url_for(url) : '')+'return false;'; //html_options.href='#'+(options ? Routes.url_for(options) : '') return this.start_tag_for('input', html_options) } EJS.Helpers.prototype.link_to_if = function(condition, name, url, html_options, post, block) { return this.link_to_unless((condition == false), name, url, html_options, post, block); } EJS.Helpers.prototype.link_to_unless = function(condition, name, url, html_options, block) { html_options = html_options || {}; if(condition) { if(block && typeof block == 'function') { return block(name, url, html_options, block); } else { return name; } } else return this.link_to(name, url, html_options); } EJS.Helpers.prototype.link_to_unless_current = function(name, url, html_options, block) { html_options = html_options || {}; return this.link_to_unless(this.is_current_page(url), name, url, html_options, block) } EJS.Helpers.prototype.password_field_tag = function(name, value, html_options) { return this.input_field_tag(name, value, 'password', html_options); } EJS.Helpers.prototype.select_tag = function(name, value, choices, html_options) { html_options = html_options || {}; html_options.id = html_options.id || name; html_options.value = value; html_options.name = name; var txt = '' txt += this.start_tag_for('select', html_options) for(var i = 0; i < choices.length; i++) { var choice = choices[i]; var optionOptions = {value: choice.value} if(choice.value == value) optionOptions.selected ='selected' txt += this.start_tag_for('option', optionOptions )+choice.text+this.tag_end('option') } txt += this.tag_end('select'); return txt; } EJS.Helpers.prototype.single_tag_for = function(tag, html_options) { return this.tag(tag, html_options, '/>');} EJS.Helpers.prototype.start_tag_for = function(tag, html_options) { return this.tag(tag, html_options); } EJS.Helpers.prototype.submit_tag = function(name, html_options) { html_options = html_options || {}; //html_options.name = html_options.id || 'commit'; html_options.type = html_options.type || 'submit'; html_options.value = name || 'Submit'; return this.single_tag_for('input', html_options); } EJS.Helpers.prototype.tag = function(tag, html_options, end) { if(!end) var end = '>' var txt = ' ' for(var attr in html_options) { if(html_options[attr] != null) var value = html_options[attr].toString(); else var value='' if(attr == "Class") // special case because "class" is a reserved word in IE attr = "class"; if( value.indexOf("'") != -1 ) txt += attr+'=\"'+value+'\" ' else txt += attr+"='"+value+"' " } return '<'+tag+txt+end; } EJS.Helpers.prototype.tag_end = function(tag) { return ''; } EJS.Helpers.prototype.text_area_tag = function(name, value, html_options) { html_options = html_options || {}; html_options.id = html_options.id || name; html_options.name = html_options.name || name; value = value || '' if(html_options.size) { html_options.cols = html_options.size.split('x')[0] html_options.rows = html_options.size.split('x')[1]; delete html_options.size } html_options.cols = html_options.cols || 50; html_options.rows = html_options.rows || 4; return this.start_tag_for('textarea', html_options)+value+this.tag_end('textarea') } EJS.Helpers.prototype.text_tag = EJS.Helpers.prototype.text_area_tag EJS.Helpers.prototype.text_field_tag = function(name, value, html_options) { return this.input_field_tag(name, value, 'text', html_options); } EJS.Helpers.prototype.url_for = function(url) { return 'window.location="'+url+'";' } EJS.Helpers.prototype.img_tag = function(image_location, alt, options){ options = options || {}; options.src = image_location options.alt = alt return this.single_tag_for('img', options) } ; // Will render a Question with multiple choices for answers. // Options format: // { // title: "Optional title for question box", // question: "Question text", // answers: [{text: "Answer text", correct: false}, ...], // singleAnswer: true, // or false, will change rendered output slightly. // singlePoint: true, // True if question give a single point score only // // if all are correct, false to give 1 point per // // correct answer. (Only for singleAnswer=false) // randomAnswers: false // Whether to randomize the order of answers. // } // // Events provided: // - h5pQuestionAnswered: Triggered when a question has been answered. var H5P = H5P || {}; /** * @typedef {Object} Options * Options for multiple choice * * @property {Object} behaviour * @property {boolean} behaviour.confirmCheckDialog * @property {boolean} behaviour.confirmRetryDialog * * @property {Object} UI * @property {string} UI.tipsLabel * * @property {Object} [confirmRetry] * @property {string} [confirmRetry.header] * @property {string} [confirmRetry.body] * @property {string} [confirmRetry.cancelLabel] * @property {string} [confirmRetry.confirmLabel] * * @property {Object} [confirmCheck] * @property {string} [confirmCheck.header] * @property {string} [confirmCheck.body] * @property {string} [confirmCheck.cancelLabel] * @property {string} [confirmCheck.confirmLabel] */ /** * Module for creating a multiple choice question * * @param {Options} options * @param {number} contentId * @param {Object} contentData * @returns {H5P.MultiChoice} * @constructor */ H5P.MultiChoice = function (options, contentId, contentData) { if (!(this instanceof H5P.MultiChoice)) return new H5P.MultiChoice(options, contentId, contentData); var self = this; this.contentId = contentId; H5P.Question.call(self, 'multichoice'); var $ = H5P.jQuery; // checkbox or radiobutton var texttemplate = '
    ' + ' <% for (var i=0; i < answers.length; i++) { %>' + '
  • ' + '
    ' + ' <%= answers[i].text %>.' + '
    ' + '
    ' + '
  • ' + ' <% } %>' + '
'; var defaults = { image: null, question: "No question text provided", answers: [ { tipsAndFeedback: { tip: '', chosenFeedback: '', notChosenFeedback: '' }, text: "Answer 1", correct: true } ], weight: 1, userAnswers: [], UI: { checkAnswerButton: 'Check', showSolutionButton: 'Show solution', tryAgainButton: 'Try again', scoreBarLabel: 'Score', tipAvailable: "Tip available", feedbackAvailable: "Feedback available", readFeedback: 'Read feedback', feedback: 'You got @score of @total points', shouldCheck: "Should have been checked", shouldNotCheck: "Should not have been checked", noInput: 'Input is required before viewing the solution' }, behaviour: { enableRetry: true, enableSolutionsButton: true, type: 'auto', singlePoint: true, randomAnswers: false, showSolutionsRequiresInput: true, disableImageZooming: false, autoCheck: false, passPercentage: 100 }, overrideSettings: {} }; var template = new EJS({text: texttemplate}); var params = $.extend(true, {}, defaults, options); // Keep track of number of correct choices var numCorrect = 0; // Loop through choices for (var i = 0; i < params.answers.length; i++) { var answer = params.answers[i]; // Make sure tips and feedback exists answer.tipsAndFeedback = answer.tipsAndFeedback || {}; if (params.answers[i].correct) { // Update number of correct choices numCorrect++; } } // Determine if no choices is the correct var blankIsCorrect = (numCorrect === 0); // Determine task type if (params.behaviour.type === 'auto') { // Use single choice if only one choice is correct params.behaviour.singleAnswer = (numCorrect === 1); } else { params.behaviour.singleAnswer = (params.behaviour.type === 'single'); } var getCheckboxOrRadioIcon = function (radio, selected) { var icon; if (radio) { icon = selected ? '' : ''; } else { icon = selected ? '' : ''; } return icon; }; // Initialize buttons and elements. var $myDom; var $feedbackDialog; /** * Remove all feedback dialogs */ var removeFeedbackDialog = function () { // Remove the open feedback dialogs. $myDom.unbind('click', removeFeedbackDialog); $myDom.find('.h5p-feedback-button, .h5p-feedback-dialog').remove(); $myDom.find('.h5p-has-feedback').removeClass('h5p-has-feedback'); if ($feedbackDialog) { $feedbackDialog.remove(); } }; var score = 0; var solutionsVisible = false; /** * Add feedback to element * @param {jQuery} $element Element that feedback will be added to * @param {string} feedback Feedback string */ var addFeedback = function ($element, feedback) { $feedbackDialog = $('' + '
' + '
' + '' + '
' + '
'); //make sure feedback is only added once if (!$element.find($('.h5p-feedback-dialog')).length) { $feedbackDialog.appendTo($element.addClass('h5p-has-feedback')); } }; /** * Register the different parts of the task with the H5P.Question structure. */ self.registerDomElements = function () { if (params.media && params.media.library) { var type = params.media.library.split(' ')[0]; if (type === 'H5P.Image') { if (params.media.params.file) { // Register task image self.setImage(params.media.params.file.path, { disableImageZooming: params.behaviour.disableImageZooming, alt: params.media.params.alt }); } } else if (type === 'H5P.Video') { if (params.media.params.sources) { // Register task video self.setVideo(params.media); } } } // Determine if we're using checkboxes or radio buttons for (var i = 0; i < params.answers.length; i++) { params.answers[i].checkboxOrRadioIcon = getCheckboxOrRadioIcon(params.behaviour.singleAnswer, params.userAnswers.indexOf(i) > -1); } // Register Introduction self.setIntroduction('
' + params.question + '
'); // Register task content area $myDom = $(template.render(params)); self.setContent($myDom, { 'class': params.behaviour.singleAnswer ? 'h5p-radio' : 'h5p-check' }); // Create tips: var $answers = $('.h5p-answer', $myDom).each(function (i) { var $tipContainer = $(this); var tip = params.answers[i].tipsAndFeedback.tip; if (tip === undefined) { return; // No tip } tip = tip.trim(); var tipContent = tip .replace(/ /g, '') .replace(/

/g, '') .replace(/<\/p>/g, '') .trim(); if (!tipContent.length) { return; // Empty tip } // Add tip var $wrap = $('

', { 'class': 'h5p-multichoice-tipwrap', 'aria-label': params.UI.tipAvailable + '.' }); var $multichoiceTip = $('
', { 'role': 'button', 'tabindex': 0, 'title': params.UI.tipsLabel, 'aria-label': params.UI.tipsLabel, 'aria-expanded': false, 'class': 'multichoice-tip', appendTo: $wrap }).click(function () { var openFeedback = !$tipContainer.children('.h5p-feedback-dialog').is($feedbackDialog); removeFeedbackDialog(); // Do not open feedback if it was open if (openFeedback) { $multichoiceTip.attr('aria-expanded', true); // Add tip dialog addFeedback($tipContainer, tip); $feedbackDialog.addClass('h5p-has-tip'); // Tip for readspeaker self.read(tip); } else { $multichoiceTip.attr('aria-expanded', false); } self.trigger('resize'); // Remove tip dialog on dom click setTimeout(function () { $myDom.click(removeFeedbackDialog); }, 100); // Do not propagate return false; }).keydown(function (e) { if (e.which === 32) { $(this).click(); return false; } }); $('.h5p-alternative-container', this).append($wrap); }); // Set event listeners. var toggleCheck = function ($ans) { if ($ans.attr('aria-disabled') === 'true') { return; } var num = parseInt($ans.data('id')); if (params.behaviour.singleAnswer) { // Store answer params.userAnswers[0] = num; // Calculate score score = (params.answers[num].correct ? 1 : 0); // De-select previous answer $answers.not($ans).removeClass('h5p-selected').attr('tabindex', '-1').attr('aria-checked', 'false'); // Select new answer $ans.addClass('h5p-selected').attr('tabindex', '0').attr('aria-checked', 'true'); } else { if ($ans.attr('aria-checked') === 'true') { // Do not allow un-checking when retry disabled and auto check if (params.behaviour.autoCheck && !params.behaviour.enableRetry) { return; } // Remove check $ans.removeClass('h5p-selected').attr('aria-checked', 'false'); } else { $ans.addClass('h5p-selected').attr('aria-checked', 'true'); } // Calculate score calcScore(); } self.triggerXAPI('interacted'); self.hideSolutions(); if (params.userAnswers.length) { self.showButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); if (params.behaviour.autoCheck) { if (params.behaviour.singleAnswer) { // Only a single answer allowed checkAnswer(); } else { // Show feedback for selected alternatives self.showCheckSolution(true); // Always finish task if it was completed successfully if (score === self.getMaxScore()) { checkAnswer(); } } } } }; $answers.click(function () { self.answered = true; toggleCheck($(this)); }).keydown(function (e) { if (e.keyCode === 32) { // Space bar // Select current item toggleCheck($(this)); return false; } if (params.behaviour.singleAnswer) { switch (e.keyCode) { case 38: // Up case 37: { // Left // Try to select previous item var $prev = $(this).prev(); if ($prev.length) { toggleCheck($prev.focus()); } return false; } case 40: // Down case 39: { // Right // Try to select next item var $next = $(this).next(); if ($next.length) { toggleCheck($next.focus()); } return false; } } } }); if (params.behaviour.singleAnswer) { // Special focus handler for radio buttons $answers.focus(function () { if ($(this).attr('aria-disabled') !== 'true') { $answers.not(this).attr('tabindex', '-1'); } }).blur(function () { if (!$answers.filter('.h5p-selected').length) { $answers.first().add($answers.last()).attr('tabindex', '0'); } }); } // Adds check and retry button addButtons(); if (!params.behaviour.singleAnswer) { calcScore(); } else { if (params.userAnswers.length && params.answers[params.userAnswers[0]].correct) { score = 1; } else { score = 0; } } // Has answered through auto-check in a previous session if (hasCheckedAnswer && params.behaviour.autoCheck) { // Check answers if answer has been given or max score reached if (params.behaviour.singleAnswer || score === self.getMaxScore()) { checkAnswer(); } else { // Show feedback for checked checkboxes self.showCheckSolution(true); } } }; this.showAllSolutions = function () { if (solutionsVisible) { return; } solutionsVisible = true; $myDom.find('.h5p-answer').each(function (i, e) { var $e = $(e); var a = params.answers[i]; if (a.correct) { $e.addClass('h5p-should').append($('', { 'class': 'h5p-solution-icon', html: params.UI.shouldCheck + '.' })); } else { $e.addClass('h5p-should-not').append($('', { 'class': 'h5p-solution-icon', html: params.UI.shouldNotCheck + '.' })); } }); // Make sure input is disabled in solution mode disableInput(); //Hide buttons and retry depending on settings. self.hideButton('check-answer'); self.hideButton('show-solution'); if (params.behaviour.enableRetry) { self.showButton('try-again'); } self.trigger('resize'); }; /** * Used in contracts. * Shows the solution for the task and hides all buttons. */ this.showSolutions = function () { removeFeedbackDialog(); self.showCheckSolution(); self.showAllSolutions(); disableInput(); self.hideButton('try-again'); }; /** * */ this.hideSolutions = function () { solutionsVisible = false; $('.h5p-answer', $myDom) .removeClass('h5p-correct') .removeClass('h5p-wrong') .removeClass('h5p-should') .removeClass('h5p-should-not') .removeClass('h5p-has-feedback'); $('.h5p-answer-icon, .h5p-solution-icon, .h5p-feedback-dialog', $myDom).remove(); this.setFeedback(); // Reset feedback self.trigger('resize'); }; /** * Resets the whole task. * Used in contracts with integrated content. * @private */ this.resetTask = function () { self.answered = false; self.hideSolutions(); params.userAnswers = []; removeSelections(); self.showButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); enableInput(); $myDom.find('.h5p-feedback-available').remove(); }; var calculateMaxScore = function () { if (blankIsCorrect) { return params.weight; } var maxScore = 0; for (var i = 0; i < params.answers.length; i++) { var choice = params.answers[i]; if (choice.correct) { maxScore += (choice.weight !== undefined ? choice.weight : 1); } } return maxScore; }; this.getMaxScore = function () { return (!params.behaviour.singleAnswer && !params.behaviour.singlePoint ? calculateMaxScore() : params.weight); }; /** * Check answer */ var checkAnswer = function () { // Unbind removal of feedback dialogs on click $myDom.unbind('click', removeFeedbackDialog); // Remove all tip dialogs removeFeedbackDialog(); self.hideButton('check-answer'); if (params.behaviour.enableSolutionsButton) { self.showButton('show-solution'); } if (params.behaviour.enableRetry) { self.showButton('try-again'); } self.showCheckSolution(); disableInput(); var xAPIEvent = self.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); addResponseToXAPI(xAPIEvent); self.trigger(xAPIEvent); self.trigger('resize'); }; /** * Determine if any of the radios or checkboxes have been checked. * * @return {boolean} */ var isAnswerSelected = function () { return !!$('.h5p-answer[aria-checked="true"]', $myDom).length; }; /** * Adds the ui buttons. * @private */ var addButtons = function () { // Show solution button self.addButton('show-solution', params.UI.showSolutionButton, function () { if (params.behaviour.showSolutionsRequiresInput && !isAnswerSelected()) { // Require answer before solution can be viewed self.updateFeedbackContent(params.UI.noInput); self.read(params.UI.noInput); } else { calcScore(); self.showAllSolutions(); } }, false); // Check solution button if (!params.behaviour.autoCheck || !params.behaviour.singleAnswer) { self.addButton('check-answer', params.UI.checkAnswerButton, function () { self.answered = true; checkAnswer(); }, true, {}, { confirmationDialog: { enable: params.behaviour.confirmCheckDialog, l10n: params.confirmCheck, instance: params.overrideSettings.instance, $parentElement: params.overrideSettings.$confirmationDialogParent } } ); } // Try Again button self.addButton('try-again', params.UI.tryAgainButton, function () { self.showButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); self.hideSolutions(); removeSelections(); enableInput(); $myDom.find('.h5p-feedback-available').remove(); self.answered = false; }, false, {}, { confirmationDialog: { enable: params.behaviour.confirmRetryDialog, l10n: params.confirmRetry, instance: params.overrideSettings.instance, $parentElement: params.overrideSettings.$confirmationDialogParent } }); }; /** * @private */ var insertFeedback = function ($e, feedback) { // Add visuals addFeedback($e, feedback); // Add button for readspeakers var $wrap = $('
', { 'class': 'h5p-hidden-read h5p-feedback-available', 'aria-label': params.UI.feedbackAvailable + '.' }); $('
', { 'role': 'button', 'tabindex': 0, 'aria-label': params.UI.readFeedback + '.', appendTo: $wrap, on: { keydown: function (e) { if (e.which === 32) { // Space self.read(feedback); return false; } } }, }); $wrap.appendTo($e); }; /** * Determine which feedback text to display * * @param {number} score * @param {number} max * @return {string} */ var getFeedbackText = function (score, max) { var feedback; var ratio = (score / max); if (isFinite(ratio) && ratio > 0) { if (ratio >= 1 && params.UI.correctText) { feedback = params.UI.correctText; } else if (params.UI.almostText) { feedback = params.UI.almostText; } } else if (params.UI.wrongText) { feedback = params.UI.wrongText; } if (!feedback) { feedback = params.UI.feedback; } return feedback.replace('@score', score).replace('@total', max); }; /** * Shows feedback on the selected fields. * @public * @param {boolean} [skipFeedback] Skip showing feedback if true */ this.showCheckSolution = function (skipFeedback) { $myDom.find('.h5p-answer').each(function (i, e) { var $e = $(e); var a = params.answers[i]; var chosen = ($e.attr('aria-checked') === 'true'); if (chosen) { if (a.correct) { $e.addClass('h5p-correct').append($('', { 'class': 'h5p-answer-icon', html: params.UI.correctAnswer + '.' })); } else { $e.addClass('h5p-wrong').append($('', { 'class': 'h5p-answer-icon', html: params.UI.wrongAnswer + '.' })); } } if (!skipFeedback) { if (chosen && a.tipsAndFeedback.chosenFeedback !== undefined && a.tipsAndFeedback.chosenFeedback !== '') { insertFeedback($e, a.tipsAndFeedback.chosenFeedback); } else if (!chosen && a.tipsAndFeedback.notChosenFeedback !== undefined && a.tipsAndFeedback.notChosenFeedback !== '') { insertFeedback($e, a.tipsAndFeedback.notChosenFeedback); } } }); // Determine feedback var max = self.getMaxScore(); // Show feedback if (!skipFeedback) { this.setFeedback(getFeedbackText(score, max), score, max, params.UI.scoreBarLabel); } // Disable task if maxscore is achieved var fullScore = (score === max); if (fullScore) { finishedTask(); } self.trigger('resize'); }; /** * Method to use when the task is correctly answered, removes all buttons and disables input. */ var finishedTask = function () { self.hideButton('check-answer'); self.hideButton('try-again'); self.hideButton('show-solution'); self.trigger('resize'); }; /** * Disables choosing new input. */ var disableInput = function () { $('.h5p-answer', $myDom).attr({ 'aria-disabled': 'true', 'tabindex': '-1' }); }; /** * Enables new input. */ var enableInput = function () { $('.h5p-answer', $myDom).attr('aria-disabled', 'false'); }; var calcScore = function () { score = 0; params.userAnswers = []; $('.h5p-answer', $myDom).each(function (idx, el) { var $el = $(el); if ($el.attr('aria-checked') === 'true') { var choice = params.answers[idx]; var weight = (choice.weight !== undefined ? choice.weight : 1); if (choice.correct) { score += weight; } else { score -= weight; } var num = parseInt($(el).data('id')); params.userAnswers.push(num); } }); if (score < 0) { score = 0; } if (!params.userAnswers.length && blankIsCorrect) { score = params.weight; } if (params.behaviour.singlePoint) { score = (100 * score / calculateMaxScore()) >= params.behaviour.passPercentage ? params.weight : 0; } }; /** * Removes selections from task. */ var removeSelections = function () { var $answers = $('.h5p-answer', $myDom) .removeClass('h5p-selected') .attr('aria-checked', 'false'); if (!params.behaviour.singleAnswer) { $answers.attr('tabindex', '0'); } else { $answers.first().attr('tabindex', '0'); } // Set focus to first option $answers.first().focus(); calcScore(); }; /** * Get xAPI data. * Contract used by report rendering engine. * * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} */ this.getXAPIData = function(){ var xAPIEvent = this.createXAPIEventTemplate('answered'); addQuestionToXAPI(xAPIEvent); addResponseToXAPI(xAPIEvent); return { statement: xAPIEvent.data.statement }; }; /** * Add the question itself to the definition part of an xAPIEvent */ var addQuestionToXAPI = function (xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); definition.description = { // Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag. 'en-US': $('
' + params.question + '
').text() }; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.interactionType = 'choice'; definition.correctResponsesPattern = []; definition.choices = []; for (var i = 0; i < params.answers.length; i++) { definition.choices[i] = { 'id': params.answers[i].originalOrder + '', 'description': { // Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag. 'en-US': $('
' + params.answers[i].text + '
').text() } }; if (params.answers[i].correct) { if (!params.singleAnswer) { if (definition.correctResponsesPattern.length) { definition.correctResponsesPattern[0] += '[,]'; // This looks insane, but it's how you separate multiple answers // that must all be chosen to achieve perfect score... } else { definition.correctResponsesPattern.push(''); } definition.correctResponsesPattern[0] += params.answers[i].originalOrder; } else { definition.correctResponsesPattern.push('' + params.answers[i].originalOrder); } } } }; /** * Add the response part to an xAPI event * * @param {H5P.XAPIEvent} xAPIEvent * The xAPI event we will add a response to */ var addResponseToXAPI = function (xAPIEvent) { var maxScore = self.getMaxScore(); var success = (100 * score / maxScore) >= params.behaviour.passPercentage; xAPIEvent.setScoredResult(score, maxScore, self, true, success); if (params.userAnswers === undefined) { calcScore(); } // Add the response var response = ''; for (var i = 0; i < params.userAnswers.length; i++) { if (response !== '') { response += '[,]'; } response += idMap === undefined ? params.userAnswers[i] : idMap[params.userAnswers[i]]; } xAPIEvent.data.statement.result.response = response; }; // Initialization code // Randomize order, if requested var idMap; // Store original order in answers for (i = 0; i < params.answers.length; i++) { params.answers[i].originalOrder = i; } if (params.behaviour.randomAnswers) { var origOrder = $.extend([], params.answers); params.answers = H5P.shuffleArray(params.answers); // Create a map from the new id to the old one idMap = []; for (i = 0; i < params.answers.length; i++) { idMap[i] = params.answers[i].originalOrder; } } // Start with an empty set of user answers. params.userAnswers = []; // Restore previous state if (contentData && contentData.previousState !== undefined) { // Restore answers if (contentData.previousState.answers) { if (!idMap) { params.userAnswers = contentData.previousState.answers; } else { // The answers have been shuffled, and we must use the id mapping. for (i = 0; i < contentData.previousState.answers.length; i++) { for (var k = 0; k < idMap.length; k++) { if (idMap[k] === contentData.previousState.answers[i]) { params.userAnswers.push(k); } } } } } } var hasCheckedAnswer = false; // Loop through choices for (var j = 0; j < params.answers.length; j++) { var ans = params.answers[j]; if (!params.behaviour.singleAnswer) { // Set role ans.role = 'checkbox'; ans.tabindex = '0'; if (params.userAnswers.indexOf(j) !== -1) { ans.checked = 'true'; hasCheckedAnswer = true; } } else { // Set role ans.role = 'radio'; // Determine tabindex, checked and extra classes if (params.userAnswers.length === 0) { // No correct answers if (i === 0 || i === params.answers.length) { ans.tabindex = '0'; } } else if (params.userAnswers.indexOf(j) !== -1) { // This is the correct choice ans.tabindex = '0'; ans.checked = 'true'; hasCheckedAnswer = true; } } // Set default if (ans.tabindex === undefined) { ans.tabindex = '-1'; } if (ans.checked === undefined) { ans.checked = 'false'; } } H5P.MultiChoice.counter = (H5P.MultiChoice.counter === undefined ? 0 : H5P.MultiChoice.counter + 1); params.role = (params.behaviour.singleAnswer ? 'radiogroup' : 'group'); params.label = 'h5p-mcq' + H5P.MultiChoice.counter; /** * Pack the current state of the interactivity into a object that can be * serialized. * * @public */ this.getCurrentState = function () { var state = {}; if (!idMap) { state.answers = params.userAnswers; } else { // The answers have been shuffled and must be mapped back to their // original ID. state.answers = []; for (var i = 0; i < params.userAnswers.length; i++) { state.answers.push(idMap[params.userAnswers[i]]); } } return state; }; /** * Check if user has given an answer. * * @param {boolean} [ignoreCheck] Ignore returning true from pressing "check-answer" button. * @return {boolean} True if answer is given */ this.getAnswerGiven = function (ignoreCheck) { var answered = ignoreCheck ? false : this.answered; return answered || params.userAnswers.length > 0 || blankIsCorrect; }; this.getScore = function () { return score; }; this.getTitle = function () { return H5P.createTitle(params.question); }; }; H5P.MultiChoice.prototype = Object.create(H5P.Question.prototype); H5P.MultiChoice.prototype.constructor = H5P.MultiChoice; ;