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 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.isHidden = function () { return ($currentSpeechBubble === undefined); }; this.remove = function () { remove(); }; 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'); } // Create bubble $currentSpeechBubble = $('
' + text + '
').appendTo($h5pContainer); // Show speech bubble with transition setTimeout(function () { $currentSpeechBubble.addClass('show'); }, 0); // Setting width to 90% of parent var width = $h5pContainer.width()*0.9; // If width is more than max width, use max width width = width > maxWidth ? maxWidth : width; var left = $container.offset().left - width + $container.outerWidth() - $h5pContainer.offset().left - ($container.width()/2) + 20; // If width makes element go outside of body, make it smaller. // TODO - This is not ideal, e.g if the $container is far to the left. // Improvement: support left- and right-"aligned" bubbles if (left < 0) { // 3px is hard coded here just to get some margin // to the left side width += left-3; left = 3; } // 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 max-width: $currentSpeechBubble.css({ width: width + 'px', top: ($container.offset().top + $container.outerHeight() - $h5pContainer.offset().top) + 'px', left: left + 'px', fontSize: fontSize + 'px' }); // Handle click to close H5P.$body.on('click.speechBubble', remove); // Handle clicks when inside IV which blocks bubbling. $container.parents('.h5p-dialog') .on('click.speechBubble', remove); 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('click.speechBubble'); $currentContainer.parents('.h5p-dialog').off('click.speechBubble'); if (iDevice) { H5P.$body.css('cursor', ''); } if ($currentSpeechBubble !== undefined) { // Apply transition, then remove speech bubble $currentSpeechBubble.removeClass('show'); 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 }; 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 */ function JoubelTip(text, params) { var speechBubble; params = $.extend({ showSpeechBubble: true }, 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, click: function () { if (speechBubble !== undefined && !speechBubble.isHidden()) { speechBubble.remove(); speechBubble = undefined; } else if (params.showSpeechBubble) { speechBubble = H5P.JoubelSpeechBubble($tip, text); } return false; } }).append($('
', { 'class': 'joubel-tip-icon' })); 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 = $('
', { 'class': 'h5p-joubel-ui-slider' }); 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 */ function JoubelProgressbar(steps) { var self = this; 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); }); } /** * Display tooltip * @method showTooltip */ JoubelProgressbar.prototype.showTooltip = function () { var self = this; if (this.currentStep === 0 || this.tooltip !== undefined) { return; } var parentWidth = 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'}); } }); } /** * 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) + '%' }); }; /** * 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 {H5P.jQuery} $container The dom container * @param {Object} params Parameters * @return {H5P.JoubelSlider} */ JoubelUI.createSlider = function ($container, 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 * @return {H5P.JoubelProgressbar} */ JoubelUI.createProgressbar = function (numSteps) { return new H5P.JoubelProgressbar(numSteps); }; /** * 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']; // 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 }; // 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; /** * 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} 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 $imgSection.addClass('h5p-question-image-fill-width'); imageThumb = false; imageTransitionTimer = setTimeout(function () { self.trigger('resize'); }, 600); } else { // Scale down image $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. } $img.attr('role', 'button').attr('tabIndex', '0'); $imgWrap.addClass('h5p-question-image-scalable') .on('click', function (event) { if (event.which === 1) { scaleImage(); // Left mouse button click } }).on('keypress', function (event) { if (event.which === 32) { scaleImage(); // 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) { // Read text from content var $el = $('
', { 'aria-live': 'assertive', 'class': 'h5p-hidden-read', 'html': content, appendTo: $wrapper }); setTimeout(function () { $el.remove(); }, 1); }; /** * 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 */ self.setFeedback = function (content, score, maxScore, scoreBarLabel) { // 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); $feedback.append($('
', { 'class': 'h5p-question-feedback-content', 'html': content })); // Feedback for readspeakers 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); }; /** * 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 */ self.addButton = function (id, text, clicked, visible, options) { 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); } } options = options || {}; buttons[id] = { isTruncated: false, text: text }; var $e = buttons[id].$element = JoubelUI.createButton($.extend({ 'class': 'h5p-question-' + id, html: text, title: text, on: { click: function () { clicked(); } } }, 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; }; /** * 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; }; /** * 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(); } self.trigger('registerDomElements'); } // Prepare container $wrapper = $container; $container.html('') .attr('role', 'application') .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); ; /*global H5P*/ H5P.Blanks = (function ($, Question) { /** * @constant * @default */ var STATE_ONGOING = 'ongoing'; var STATE_CHECKING = 'checking'; var STATE_SHOWING_SOLUTION = 'showing-solution'; var STATE_FINISHED = 'finished'; /** * Initialize module. * * @class H5P.Blanks * @extends H5P.Question * @param {Object} params Behavior settings * @param {number} id Content identification * @param {Object} contentData Task specific content data */ function Blanks(params, id, contentData) { var self = this; // Inheritance Question.call(self, 'blanks'); // IDs this.contentId = id; // Set default behavior. this.params = $.extend(true, {}, { text: "Fill in", questions: [ "Oslo is the capital of *Norway*." ], userAnswers: [], score: "You got @score of @total points.", showSolutions: "Show solutions", tryAgain: "Try again", checkAnswer: "Check", changeAnswer: "Change answer", notFilledOut: "Please fill in all blanks", behaviour: { enableRetry: true, enableSolutionsButton: true, caseSensitive: true, showSolutionsRequiresInput: true, autoCheck: false, separateLines: false, disableImageZooming: false } }, params); // Delete empty questions for (var i = this.params.questions.length - 1; i >= 0; i--) { if (!this.params.questions[i]) { this.params.questions.splice(i, 1); } } // Previous state this.contentData = contentData; if (this.contentData !== undefined && this.contentData.previousState !== undefined) { this.previousState = this.contentData.previousState; } // Clozes this.clozes = []; // Keep track tabbing forward or backwards this.shiftPressed = false; H5P.$body.keydown(function (event) { if (event.keyCode === 16) { self.shiftPressed = true; } }).keyup(function (event) { if (event.keyCode === 16) { self.shiftPressed = false; } }); } // Inheritance Blanks.prototype = Object.create(Question.prototype); Blanks.prototype.constructor = Blanks; /** * Registers this question type's DOM elements before they are attached. * Called from H5P.Question. */ Blanks.prototype.registerDomElements = function () { var self = this; if (self.params.image) { // Register task image self.setImage(self.params.image.path, {disableImageZooming: self.params.behaviour.disableImageZooming}); } // Register task introduction text self.setIntroduction(self.params.text); // Register task content area self.setContent(self.createQuestions(), { 'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : '' }); // ... and buttons self.registerButtons(); // Restore previous state self.setH5PUserState(); }; /** * Create all the buttons for the task */ Blanks.prototype.registerButtons = function () { var self = this; if (!self.params.behaviour.autoCheck) { // Check answer button self.addButton('check-answer', self.params.checkAnswer, function () { self.toggleButtonVisibility(STATE_CHECKING); self.markResults(); self.showEvaluation(); self.triggerAnswered(); }); } // Check answer button self.addButton('show-solution', self.params.showSolutions, function () { if (self.allBlanksFilledOut()) { self.toggleButtonVisibility(STATE_SHOWING_SOLUTION); self.showCorrectAnswers(); } }, self.params.behaviour.enableSolutionsButton); // Try again button if (self.params.behaviour.enableRetry === true) { self.addButton('try-again', self.params.tryAgain, function () { self.resetTask(); self.$questions.filter(':first').find('input:first').focus(); }); } self.toggleButtonVisibility(STATE_ONGOING); }; /** * Find blanks in a string and run a handler on those blanks * * @param {string} question * Question text containing blanks enclosed in asterisks. * @param {function} handler * Replaces the blanks text with an input field. * @returns {string} * The question with blanks replaced by the given handler. */ Blanks.prototype.handleBlanks = function (question, handler) { // Go through the text and run handler on all asterix var clozeEnd, clozeStart = question.indexOf('*'); var self = this; while (clozeStart !== -1 && clozeEnd !== -1) { clozeStart++; clozeEnd = question.indexOf('*', clozeStart); if (clozeEnd === -1) { continue; // No end } var replacer = handler(self.parseSolution(question.substring(clozeStart, clozeEnd))); clozeEnd++; question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd); clozeEnd -= clozeEnd - clozeStart - replacer.length; // Find the next cloze clozeStart = question.indexOf('*', clozeEnd); } return question; }; /** * Create questitons html for DOM */ Blanks.prototype.createQuestions = function () { var self = this; var html = ''; for (var i = 0; i < self.params.questions.length; i++) { var question = self.params.questions[i]; // Go through the question text and replace all the asterisks with input fields question = self.handleBlanks(question, function(solution) { // Create new cloze var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null); var cloze = new Blanks.Cloze(solution, self.params.behaviour, defaultUserAnswer); self.clozes.push(cloze); return cloze; }); html += '
' + question + '
'; } this.$questions = $(html); // Set input fields. this.$questions.find('input').each(function (i) { var afterCheck; if (self.params.behaviour.autoCheck) { afterCheck = function () { if (self.done || self.getAnswerGiven()) { // All answers has been given. Show solutions button. self.toggleButtonVisibility(STATE_CHECKING); self.showEvaluation(); self.done = true; self.triggerAnswered(); } }; } self.clozes[i].setInput($(this), afterCheck, function () { self.toggleButtonVisibility(STATE_ONGOING); if (!self.params.behaviour.autoCheck) { self.hideEvaluation(); } }); }).keydown(function (event) { self.autoGrowTextField($(this)); if (event.keyCode === 13) { return false; // Prevent form submission on enter key } // Refocus buttons after they have been toggled if last input if (event.keyCode === 9 && self.params.behaviour.autoCheck) { var $inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input'); var $lastInput = $inputs[$inputs.length - 1]; if ($(this).is($lastInput) && !self.shiftPressed) { setTimeout(function () { self.focusButton(); }, 10); } } }).on('change', function () { self.triggerXAPI('interacted'); }); self.on('resize', function () { self.resetGrowTextField(); }); return this.$questions; }; /** * */ Blanks.prototype.autoGrowTextField = function ($input) { // Do not set text field size when separate lines is enabled if (this.params.behaviour.separateLines) { return; } var self = this; var fontSize = parseInt($input.css('font-size'), 10); var minEm = 3; var minPx = fontSize * minEm; var rightPadEm = 3.25; var rightPadPx = fontSize * rightPadEm; var static_min_pad = 0.5 * fontSize; setTimeout(function(){ var tmp = $('
', { 'html': $input.val() }); tmp.css({ 'position': 'absolute', 'white-space': 'nowrap', 'font-size': $input.css('font-size'), 'font-family': $input.css('font-family'), 'padding': $input.css('padding'), 'width': 'initial' }); $input.parent().append(tmp); var width = tmp.width(); var parentWidth = self.$questions.width(); tmp.remove(); if (width <= minPx) { // Apply min width $input.width(minPx + static_min_pad); } else if (width + rightPadPx >= parentWidth) { // Apply max width of parent $input.width(parentWidth - rightPadPx); } else { // Apply width that wraps input $input.width(width + static_min_pad); } }, 1); }; /** * Resize all text field growth to current size. */ Blanks.prototype.resetGrowTextField = function () { var self = this; this.$questions.find('input').each(function () { self.autoGrowTextField($(this)); }); }; /** * Toggle buttons dependent of state. * * Using CSS-rules to conditionally show/hide using the data-attribute [data-state] */ Blanks.prototype.toggleButtonVisibility = function (state) { // The show solutions button is hidden if all answers are correct var allCorrect = (this.getScore() === this.getMaxScore()); if (this.params.behaviour.autoCheck && allCorrect) { // We are viewing the solutions state = STATE_FINISHED; } if (this.params.behaviour.enableSolutionsButton) { if (state === STATE_CHECKING && !allCorrect) { this.showButton('show-solution'); } else { this.hideButton('show-solution'); } } if (this.params.behaviour.enableRetry) { if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) { this.showButton('try-again'); } else { this.hideButton('try-again'); } } if (state === STATE_ONGOING) { this.showButton('check-answer'); } else { this.hideButton('check-answer'); } this.trigger('resize'); }; /** * Check if all blanks are filled out. Warn user if not */ Blanks.prototype.allBlanksFilledOut = function () { var self = this; if (!self.getAnswerGiven()) { this.updateFeedbackContent(self.params.notFilledOut); return false; } return true; }; /** * Mark which answers are correct and which are wrong and disable fields if retry is off. */ Blanks.prototype.markResults = function () { var self = this; for (var i = 0; i < self.clozes.length; i++) { self.clozes[i].checkAnswer(); if (!self.params.behaviour.enableRetry) { self.clozes[i].disableInput(); } } this.trigger('resize'); }; /** * Removed marked results */ Blanks.prototype.removeMarkedResults = function () { this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong'); this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false); this.trigger('resize'); }; /** * Displays the correct answers */ Blanks.prototype.showCorrectAnswers = function () { var self = this; this.hideSolutions(); for (var i = 0; i < self.clozes.length; i++) { self.clozes[i].showSolution(); } this.trigger('resize'); }; /** * Display the correct solution for the input boxes. * * This is invoked from CP - be carefull! */ Blanks.prototype.showSolutions = function () { this.params.behaviour.enableSolutionsButton = true; this.toggleButtonVisibility(STATE_FINISHED); this.markResults(); this.showCorrectAnswers(); this.showEvaluation(); //Hides all buttons in "show solution" mode. this.hideButtons(); }; /** * Resets the complete task. * Used in contracts. * @public */ Blanks.prototype.resetTask = function () { this.hideEvaluation(); this.hideSolutions(); this.clearAnswers(); this.removeMarkedResults(); this.toggleButtonVisibility(STATE_ONGOING); this.resetGrowTextField(); this.done = false; }; /** * Hides all buttons. * @public */ Blanks.prototype.hideButtons = function () { this.toggleButtonVisibility(STATE_FINISHED); }; /** * Trigger xAPI answered event */ Blanks.prototype.triggerAnswered = function() { var xAPIEvent = this.createXAPIEventTemplate('answered'); this.addQuestionToXAPI(xAPIEvent); this.addResponseToXAPI(xAPIEvent); this.trigger(xAPIEvent); }; /** * Add the question itselt to the definition part of an xAPIEvent */ Blanks.prototype.addQuestionToXAPI = function(xAPIEvent) { var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); definition.description = { 'en-US': this.params.text }; definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction'; definition.interactionType = 'fill-in'; definition.correctResponsesPattern = ['{case_matters=' + this.params.behaviour.caseSensitive + '}']; var firstCorrectResponse = true; // xAPI forces us to create solution patterns for all possible solution combinations for (var i = 0; i < this.params.questions.length; i++) { var question = this.handleBlanks(this.params.questions[i], function(solution) { // Store new patterns for each extra alternative answer var newPatterns = []; for (var j = 0; j < definition.correctResponsesPattern.length; j++) { if (!firstCorrectResponse) { definition.correctResponsesPattern[j] += '[,]'; } var prefix = definition.correctResponsesPattern[j]; for (var k = 0; k < solution.solutions.length; k++) { if (k === 0) { // This is the first possible answr, just add it to the pattern definition.correctResponsesPattern[j] += solution.solutions[k]; } else { // This is an alternative possible answer, we need to create a new permutation newPatterns.push(prefix + solution.solutions[k]) } } } // Add any new permutations to the list of response patterns definition.correctResponsesPattern = definition.correctResponsesPattern.concat(newPatterns); firstCorrectResponse = false; // We replace the solutions in the question with a "blank" return '__________'; }); definition.description['en-US'] += question; } }; /** * Parse the solution text (text between the asterix) * * @param {string} solutionText * @returns {object} with the following properties * - tip: the tip text for this solution, undefined if no tip * - solutions: array of solution words */ Blanks.prototype.parseSolution = function (solutionText) { var solutions = []; var tip; var solutionsAndTip = solutionText.split(':'); if (solutionsAndTip.length > 0) { solutions = solutionsAndTip[0].split('/'); } if (solutionsAndTip.length === 2) { tip = solutionsAndTip[1]; } // Trim solutions for (var i = 0; i < solutions.length; i++) { solutions[i] = H5P.trim(solutions[i]); if (this.params.behaviour.caseSensitive !== true) { solutions[i] = solutions[i].toLowerCase(); } } return { tip: tip, solutions: solutions }; } /** * Add the response part to an xAPI event * * @param {H5P.XAPIEvent} xAPIEvent * The xAPI event we will add a response to */ Blanks.prototype.addResponseToXAPI = function (xAPIEvent) { xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this); var usersAnswers = this.getCurrentState(); xAPIEvent.data.statement.result.response = usersAnswers.join('[,]'); }; /** * Show evaluation widget, i.e: 'You got x of y blanks correct' */ Blanks.prototype.showEvaluation = function () { var maxScore = this.getMaxScore(); var score = this.getScore(); var scoreText = this.params.score.replace('@score', score).replace('@total', maxScore); this.setFeedback(scoreText, score, maxScore); if (score === maxScore) { this.toggleButtonVisibility(STATE_FINISHED); } }; /** * Hide the evaluation widget */ Blanks.prototype.hideEvaluation = function () { // Clear evaluation section. this.setFeedback(); }; /** * Hide solutions. (/try again) */ Blanks.prototype.hideSolutions = function () { // Clean solution from quiz this.$questions.find('.h5p-correct-answer').remove(); }; /** * Get maximum number of correct answers. * * @returns {Number} Max points */ Blanks.prototype.getMaxScore = function () { var self = this; return self.clozes.length; }; /** * Count the number of correct answers. * * @returns {Number} Points */ Blanks.prototype.getScore = function () { var self = this; var correct = 0; for (var i = 0; i < self.clozes.length; i++) { if (self.clozes[i].correct()) { correct++; } self.params.userAnswers[i] = self.clozes[i].getUserAnswer(); } return correct; }; Blanks.prototype.getTitle = function() { return H5P.createTitle(this.params.text); }; /** * Clear the user's answers */ Blanks.prototype.clearAnswers = function () { this.$questions.find('.h5p-text-input').val(''); }; /** * Checks if all has been answered. * * @returns {Boolean} */ Blanks.prototype.getAnswerGiven = function () { var self = this; if (this.params.behaviour.showSolutionsRequiresInput === true) { for (var i = 0; i < self.clozes.length; i++) { if (!self.clozes[i].filledOut()) { return false; } } } return true; }; /** * Helps set focus the given input field. * @param {jQuery} $input */ Blanks.setFocus = function ($input) { setTimeout(function () { $input.focus(); }, 1); }; /** * Returns an object containing content of each cloze * * @returns {object} object containing content for each cloze */ Blanks.prototype.getCurrentState = function () { var clozesContent = []; // Get user input for every cloze this.clozes.forEach(function (cloze) { clozesContent.push(cloze.getUserInput()); }); return clozesContent; }; /** * Sets answers to current user state */ Blanks.prototype.setH5PUserState = function () { var self = this; var isValidState = (this.previousState !== undefined && this.previousState.length && this.previousState.length === this.clozes.length); // Check that stored user state is valid if (!isValidState) { return; } // Set input from user state var hasAllClozesFilled = true; this.previousState.forEach(function (clozeContent, ccIndex) { var cloze = self.clozes[ccIndex]; cloze.setUserInput(clozeContent); // Handle instant feedback if (self.params.behaviour.autoCheck) { if (cloze.filledOut()) { cloze.checkAnswer(); } else { hasAllClozesFilled = false; } } }); if (self.params.behaviour.autoCheck && hasAllClozesFilled) { self.showEvaluation(); self.toggleButtonVisibility(STATE_CHECKING); } }; /** * Disables any active input. Useful for freezing the task and dis-allowing * modification of wrong answers. */ Blanks.prototype.disableInput = function () { this.$questions.find('input').attr('disabled', true); }; return Blanks; })(H5P.jQuery, H5P.Question); ; (function ($, Blanks) { /** * Simple private class for keeping track of clozes. * * @class H5P.Blanks.Cloze * @param {string} answer * @param {Object} behaviour Behaviour for the task * @param {string} defaultUserAnswer */ Blanks.Cloze = function (solution, behaviour, defaultUserAnswer) { var self = this; var $input, $wrapper; var answers = solution.solutions; var answer = answers.join('/'); var tip = solution.tip; /** * Check if the answer is correct. * * @private * @param {string} answered */ var correct = function (answered) { if (behaviour.caseSensitive !== true) { answered = answered.toLowerCase(); } for (var i = 0; i < answers.length; i++) { if (answered === answers[i]) { return true; } } return false; }; /** * Check if filled out. * * @param {boolean} */ this.filledOut = function () { var answered = this.getUserAnswer(); // Blank can be correct and is interpreted as filled out. return (answered !== '' || correct(answered)); }; /** * Check the cloze and mark it as wrong or correct. */ this.checkAnswer = function () { var isCorrect = correct(this.getUserAnswer()); if (isCorrect) { $wrapper.addClass('h5p-correct'); $input.attr('disabled', true); } else { $wrapper.addClass('h5p-wrong'); } }; /** * Disables further input. */ this.disableInput = function () { $input.attr('disabled', true); }; /** * Show the correct solution. */ this.showSolution = function () { if (correct(this.getUserAnswer())) { return; // Only for the wrong ones } $(' ' + answer + '').insertAfter($wrapper); $input.attr('disabled', true); }; /** * @returns {boolean} */ this.correct = function () { return correct(this.getUserAnswer()); }; /** * Set input element. * * @param {H5P.jQuery} $element * @param {function} afterCheck * @param {function} afterFocus */ this.setInput = function ($element, afterCheck, afterFocus) { $input = $element; $wrapper = $element.parent(); // Add tip if tip is set if(tip !== undefined && tip.trim().length > 0) { $wrapper.addClass('has-tip').append(H5P.JoubelUI.createTip(tip, $wrapper.parent())); } if (afterCheck !== undefined) { $input.blur(function () { if (self.filledOut()) { // Check answers if (!behaviour.enableRetry) { self.disableInput(); } self.checkAnswer(); afterCheck(); } }); } $input.focus(function () { $wrapper.removeClass('h5p-wrong'); if (afterFocus !== undefined) { afterFocus(); } }); }; /** * @returns {string} Cloze html */ this.toString = function () { var extra = defaultUserAnswer ? ' value="' + defaultUserAnswer + '"' : ''; return ''; }; /** * @returns {string} Trimmed answer */ this.getUserAnswer = function () { return H5P.trim($input.val()); }; /** * @returns {string} Answer */ this.getUserInput = function () { return $input.val(); }; /** * @param {string} text New input text */ this.setUserInput = function (text) { $input.val(text); }; }; })(H5P.jQuery, H5P.Blanks); ;